目錄
一 前景導入
1 當前讀
2 快照讀
二 MVCC
1 隱藏字段
2 UndoLog 回滾日志
(1 UndoLog日志
(2 UndoLog版本鏈
3 Read?View
面試八股
?介紹一下MVCC
一 前景導入
1 當前讀
可使當前事務讀取的是最新版本的數據,讀取時還要保證其他并發事務不能修改當中記錄,會對讀取的記錄進行加鎖。
-
SELECT ... LOCK IN SHARE MODE
?(共享鎖/S鎖) -
SELECT ... FOR UPDATE
?(排他鎖/X鎖) -
UPDATE
、INSERT
、DELETE
?操作(自動加排他鎖)
2 快照讀
簡單的select(不加鎖)就是快照讀,讀取的是記錄數據的可見版本,有可能是歷史數據,不加鎖。
快照讀本質上就是使用MVCC機制訪問數據的歷史版本
隔離級別 | 快照讀行為 | 當前讀行為 |
---|---|---|
Read Committed | 每次 SELECT 都生成新的快照(能看到其他事務已提交的修改) | 始終讀取最新已提交版本并加鎖 |
Repeatable Read | 事務中第一個 SELECT 語句建立快照,后續讀取都基于此快照(看不到后續修改) | 始終讀取最新已提交版本并加鎖 |
Serializable | 快照讀退化為當前讀(所有 SELECT 自動轉為 SELECT ... LOCK IN SHARE MODE) | 正常當前讀行為 |
二 MVCC
概念:MVCC全稱:多版本并發控制。
MVCC允許多個事務同時讀取同一行數據,但是確保了數據版本是當前事務開啟之前的版本。(其他事務修改但是看見的版本還是修改之前的)
1 隱藏字段
在 InnoDB 的 MVCC 實現中,每條記錄都包含三個關鍵隱藏字段,它們共同構建了多版本控制的基石:
字段名 | 大小 | 作用 | 是否必選 |
---|---|---|---|
DB_TRX_ID?事務ID | 6 字節 | 記錄最后修改該行的事務 ID | ?總是存在 |
DB_ROLL_PTR?回滾指針 | 7 字節 | 指向 Undo Log 中上一個版本的指針(構成版本鏈) | ?總是存在 |
DB_ROW_ID | 6 字節 | 隱式自增主鍵(僅當無主鍵時生成) | ?條件存在 |
2 UndoLog 回滾日志
(1 UndoLog日志
概念:回滾日志,用于記錄數據被修改前的信息
作用:是數據庫實現事務原子性和多版本并發控制的核心機制
場景化描述:
當事務需要回滾或一致性讀時,內存中可能存在未提交的修改。
重啟后或事務內,Undo Log 能提供舊數據版本,用于:
撤銷未提交的操作(回滾)
構造歷史快照(MVCC 非阻塞讀)
(2 UndoLog版本鏈
Undo Log 版本鏈的核心價值正是通過精準的指針定位實現對歷史版本的精確訪問。
3 Read?View
讀視圖,用于決定事務能看到哪些版本的數據。本質上是事務啟動時對數據庫系統狀態的一次快照,解決了并發讀寫當中的數據可見性問題。
Read View 包含四個關鍵字段:
字段名 | 描述 | 作用 |
---|---|---|
m_ids | 生成 Read View 時活躍事務ID列表(未提交的事務) | 判斷數據版本是否由未提交事務創建 |
min_trx_id | 活躍事務中的最小事務ID | 加速判斷:事務ID < min_trx_id 一定可見 |
max_trx_id | 系統預分配的下一個事務ID(非當前最大ID) | 判斷:事務ID ≥ max_trx_id 一定不可見 |
creator_trx_id | 創建該 Read View 的事務ID(當前事務自身ID) | 避免看到自己未提交的修改 |
讀已提交這個隔離級別當中,在每一個select 語句執行前都會生成一個ReadView。導致出現不可重復讀的現象,可能出現兩次讀取數據不同的情況。
可重復讀是執行第一條select時,生成一個ReadView然后整個業務期間都在使用這個ReadView。讀取的數據始終相同,無視其他事務的提交。
舉個例子
-- 事務A (RC級別)
BEGIN; -- trx_id=100 (事務A被分配ID=100)-- 第一次查詢 (創建ReadView1)
SELECT balance FROM accounts WHERE id=1; -- 返回1000
/*
ReadView1狀態:m_ids = [100] -- 活躍事務ID列表 (當前只有事務A)min_trx_id = 100 -- 最小活躍事務ID (m_ids中的最小值)max_trx_id = 101 -- 下一個將分配的事務ID (當前最大事務ID+1)creator_trx_id = 100 -- 創建此ReadView的事務ID可見性判斷過程:假設數據行初始db_trx_id=90 (小于min_trx_id)90 < min_trx_id(100) → 可見 → 返回1000
*/-- 事務B (trx_id=101) 啟動并提交UPDATE accounts SET balance=900 WHERE id=1;-- 修改后數據行:-- db_trx_id = 101 (最后修改事務ID)-- db_roll_ptr → 指向舊版本(trx_id=90, balance=1000)COMMIT; -- 事務B提交,從活躍事務列表移除-- 第二次查詢 (創建新ReadView2)
SELECT balance FROM accounts WHERE id=1; -- 返回900
/*
ReadView2狀態:m_ids = [100] -- 活躍事務ID列表 (事務B已提交,只剩事務A)min_trx_id = 100 -- 最小活躍事務ID (仍是100)max_trx_id = 102 -- 下一個將分配的事務ID (101已使用)creator_trx_id = 100 -- 創建者事務ID可見性判斷過程:當前行db_trx_id=1011. 101 != creator_trx_id(100) → 非當前事務修改2. 101 >= min_trx_id(100) 且 101 < max_trx_id(102) → 在[mins, max)范圍內3. 檢查m_ids=[100] → 101不在其中 → 已提交 → 可見返回當前版本數據900
*/
可見性判斷規則優先級:
-
首先檢查:
db_trx_id == creator_trx_id
(當前事務自身修改) -
然后檢查:
db_trx_id < min_trx_id
(在ReadView創建前已提交) -
再檢查:
db_trx_id >= max_trx_id
(在ReadView創建后啟動的事務)讀取快照之后有事務過來修改了但是快照讀取的是那一瞬間的值故才會出現大于max預分配的情況 -
最后檢查范圍:
min_trx_id <= db_trx_id < max_trx_id
-
在m_ids中 → 未提交 → 不可見
-
不在m_ids中 → 已提交 → 可見
-
三 面試八股
?介紹一下MVCC
首先我想介紹的是MVCC是什么,MVCC全稱多版本并發控制,核心思想如同字面意思,為數據維護多個版本,而并非直接覆蓋。
其次再說說其功能,其主要解決的是讀寫之間沖突而導致的并發性能問題。他讓不同的事務在不同的隔離級別能看見不同的隔離級別,主要是RC與RR這兩種隔離級別(RC是在每一次讀取之前都會生成一個ReadView快照,而RR是只在第一次讀取之前生成一個ReadView快照,RR則在一次事務當中就不會出現不可重復讀的情況,而RC則會出現不可重復讀的現象(因為其允許其他事務對其進行寫的操作,導致讀取的數據可能會出現不同,呆滯出現不可重復讀))
核心:版本鏈+ReadView+undolog
這里說到了ReadView就涉及其原理部分了,這里就不得不提到一個隱藏字段,存儲在數據行當中的db_trx_id(最后修改數據的事務id),在讀取時會拿這個事務id與ReadView當中的字段進行對比,四個字段(創建當前ReadView事務的id,生成ReadView時活躍的最小事務id,生成ReadView時活躍的事務id的列表,生成ReadView時預分配的下一個事務id)
-
首先檢查:
db_trx_id == creator_trx_id
(當前事務自身修改)可見 -
然后檢查:
db_trx_id < min_trx_id
(在ReadView創建前已提交)可見 -
再檢查:
db_trx_id >= max_trx_id
(在ReadView創建后啟動的事務)不可見 -
最后檢查范圍:
min_trx_id <= db_trx_id < max_trx_id
-
在m_ids中 → 未提交 → 不可見
-
不在m_ids中 → 已提交 → 可見
-
以上是對事務版本的判斷,判斷結束后如果是可見的,那么就直接返回該版本的數據作為查詢結果,但是如果不可見,那么就需要用到版本鏈回溯到之前的版本,獲取行數據的db_roll_ptr(回滾指針),指針指向上一個版本在undolog(回滾日志)當中具體位置