1. 多個undo log形成的鏈表
InnoDB存儲引擎中,它的聚簇索引記錄中都包含兩個必要的隱藏列,分別是:
- trx_id:事務(wù)Id,每次一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,都會把該事務(wù)的事務(wù)id
賦值給trx_id
隱藏列。 - roll_pointer:回滾指針,每次對某條聚簇索引記錄進(jìn)行改動時,都會把舊的版本寫入到undo log
中,然后這個隱藏列就相當(dāng)于一個指針,可以通過它來找到該記錄修改前的信息。
每個事務(wù)都會修改一組Undo Record,這些Undo Record首位相連就組成了這個事務(wù)的Undo Log。同一個Record被不同的事務(wù)修改,會產(chǎn)生不同的歷史版本,這些歷史版本又通過Rollptr串成一個鏈表,供MVCC使用:

同時可以看出:Insert類型的Undo Record中記錄了對應(yīng)的主鍵值:id=1,而Update類型的Undo Record中還記錄了對應(yīng)的歷史版本的生成事務(wù)Trx_id,以及被修改的field a的歷史值。
2. MVCC版本控制
多版本的目的是為了避免寫事務(wù)和讀事務(wù)的互相等待,那么每個讀事務(wù)都需要在不對Record加鎖的情況下,找到對應(yīng)的應(yīng)該看到的歷史版本。
InnoDB的做法是(RR隔離級別):
- 在事務(wù)第一次讀取的時候(select)獲取一份ReadView,并一直持有;
- 在事務(wù)第一次DML語句(Insert、update、delete)時,為該事務(wù)分配全局的事務(wù)id;
場景1:事務(wù)開始前:id=1的trx_id為10
| session1 | session2 | session3 | session4 |
|---|---|---|---|
| begin; | begin; | begin; | begin; |
| update id=10;(trx_id=11) | |||
| update id=1;(trx_id=12) | |||
| commit; | |||
| update id=1;(trx_id=11) | |||
| select id=1;(生成ReadView) | |||
| commit; | |||
| update id=1;(trx_id=13) | |||
| select id=1;(結(jié)論?) |
2.1 基礎(chǔ)概念
ReadView可以理解為數(shù)據(jù)庫中某一個時刻所有未提交事務(wù)的快照。ReadView有如下幾個重要的參數(shù):
- m_ids:表示生成ReadView時,當(dāng)前系統(tǒng)正在活躍的寫事務(wù)Id列表(未提交的事務(wù));
- min_trx_id:表示生成ReadView時,當(dāng)前系統(tǒng)中活躍的寫事務(wù)的最小事務(wù)Id;
- max_trx_id:表示生成ReadView時,當(dāng)前時間戳InnoDB將在下一次分配的事務(wù)Id;
- creator_trx_id:當(dāng)前事務(wù)Id;
判斷邏輯:
- 如果被訪問版本的trx_id等于creator_trx_id,表示當(dāng)前事務(wù)在訪問自己修改的記錄,所以該版本可以被當(dāng)前事務(wù)訪問;
- 如果被訪問版本trx_id屬性值小于ReadView的最小事務(wù)Id,表示該版本的事務(wù)在生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問;
- 如果被訪問版本trx_id屬性值大于等于ReadView的最大事務(wù)Id,表示該版本的事務(wù)在生成ReadView后才活躍狀態(tài),所以該版本不可以被當(dāng)前事務(wù)訪問;
- 如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那么就需要判斷:
3.1 trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;
3.2 trx_id如果不在,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)提交,該版本可以被訪問。
2.2 場景1分析
session3執(zhí)行時:
- 活躍事務(wù)列表m_id:[11];
- 活躍的最小-最大事務(wù)Id:[11,13];
- 當(dāng)前事務(wù)Id,因?yàn)橹皇亲x事務(wù),未分配;
在session3生成ReadView時,多個事務(wù)產(chǎn)生的多個undo log被roll_pointer串聯(lián)起來的鏈表簡化圖如下:

session3執(zhí)行select id=1語句時,
- 查詢到被訪問記錄的trx_id=11,在最大-最小事務(wù)Id之間[11,13],且trx_id在m_ids[11]中,該版本不能被訪問;
- 那么mvcc版本鏈中查詢到trx_id=12,在min_trx_id和max_trx_id之間[11,13],但是trx_id不在m_ids[12]中,那么該版本可以被訪問。
session4執(zhí)行時:

對,undo log是在update開始時執(zhí)行時就生成的,而不是在commit后才生成undo log。
session3第二次執(zhí)行select id=1語句時:
- 查詢到被訪問記錄的trx_id=13,等于max_trx_id,說明是在ReadView后才開啟的,不能訪問該版本;
- 那么mvcc版本鏈中查詢到被訪問記錄的trx_id=11,在最大-最小事務(wù)Id之間[11,13],且trx_id在m_ids[11]中,該版本不能被訪問;
- 那么mvcc版本鏈中查詢到trx_id=12,在min_trx_id和max_trx_id之間[11,13],但是trx_id不在m_ids[12]中,那么該版本可以被訪問。
3. MVCC為什么不能解決幻讀
3.1 場景2
| session1 | session2 |
|---|---|
| begin; | begin; |
| insert into test(id,xid) values(2,2) ;(trx_id=10) | |
| commit; | |
| select * from test where id=2;(開啟ReadView) | |
| commit; |
id=2的undo log形成的版本鏈:

- 活躍id列表m_ids[]
- [min_trx_id,max_trx_id]為[11,12)
- 被訪問的id=1中trx_id=10,小于min_trx_id故可以被訪問到。
因此出現(xiàn)了幻讀;
3.3 場景3
| session1 | session2 |
|---|---|
| begin; | begin; |
| select * from test;(開啟ReadView) | |
| insert into test(id,xid) values(3,3) ;(trx_id=14) | |
| commit; | |
| select * from test where id =3;(第一次查詢,null) | |
| update test set xid=30 where id=3 ;(trx_id=15) | |
| select * from test where id =3;(第二次查詢,not null) | |
| commit; |
開啟ReadView時:
- 活躍id列表m_ids[]
- [min_trx_id,max_trx_id]為[10,12)
- session1的第一次查詢時,id=3的trx_id=14大于max_trx_id,說明是生成ReadView后才生成的記錄,不可以訪問到;
- session1的update操作,session1事務(wù)生成trx_id=15,因?yàn)楦耰d=3,所以id=3的Record也是trx_id=15;
- session1的第二次查詢時,id=3的trx_id=15等于creator_trx_id(自己的事務(wù)id),可以被訪問到;
因此再次出現(xiàn)了幻讀。