【精】使用各種場景徹底明白mysql的MVCC原理

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使用:

MVCC原理.png

同時可以看出: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ù):

  1. m_ids:表示生成ReadView時,當(dāng)前系統(tǒng)正在活躍的寫事務(wù)Id列表(未提交的事務(wù));
  2. min_trx_id:表示生成ReadView時,當(dāng)前系統(tǒng)中活躍的寫事務(wù)的最小事務(wù)Id;
  3. max_trx_id:表示生成ReadView時,當(dāng)前時間戳InnoDB將在下一次分配的事務(wù)Id;
  4. creator_trx_id:當(dāng)前事務(wù)Id;

判斷邏輯:

  1. 如果被訪問版本的trx_id等于creator_trx_id,表示當(dāng)前事務(wù)在訪問自己修改的記錄,所以該版本可以被當(dāng)前事務(wù)訪問;
  2. 如果被訪問版本trx_id屬性值小于ReadView的最小事務(wù)Id,表示該版本的事務(wù)在生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問;
  3. 如果被訪問版本trx_id屬性值大于等于ReadView的最大事務(wù)Id,表示該版本的事務(wù)在生成ReadView后才活躍狀態(tài),所以該版本不可以被當(dāng)前事務(wù)訪問;
  4. 如果被訪問版本的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í)行時:

  1. 活躍事務(wù)列表m_id:[11];
  2. 活躍的最小-最大事務(wù)Id:[11,13];
  3. 當(dāng)前事務(wù)Id,因?yàn)橹皇亲x事務(wù),未分配;

在session3生成ReadView時,多個事務(wù)產(chǎn)生的多個undo log被roll_pointer串聯(lián)起來的鏈表簡化圖如下:

mvcc版本鏈.png

session3執(zhí)行select id=1語句時,

  1. 查詢到被訪問記錄的trx_id=11,在最大-最小事務(wù)Id之間[11,13],且trx_id在m_ids[11]中,該版本不能被訪問;
  2. 那么mvcc版本鏈中查詢到trx_id=12,在min_trx_id和max_trx_id之間[11,13],但是trx_id不在m_ids[12]中,那么該版本可以被訪問。

session4執(zhí)行時:

mvcc版本鏈.png

對,undo log是在update開始時執(zhí)行時就生成的,而不是在commit后才生成undo log。

session3第二次執(zhí)行select id=1語句時:

  1. 查詢到被訪問記錄的trx_id=13,等于max_trx_id,說明是在ReadView后才開啟的,不能訪問該版本;
  2. 那么mvcc版本鏈中查詢到被訪問記錄的trx_id=11,在最大-最小事務(wù)Id之間[11,13],且trx_id在m_ids[11]中,該版本不能被訪問;
  3. 那么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形成的版本鏈:

image.png
  1. 活躍id列表m_ids[]
  2. [min_trx_id,max_trx_id]為[11,12)
  3. 被訪問的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時:

  1. 活躍id列表m_ids[]
  2. [min_trx_id,max_trx_id]為[10,12)
  3. session1的第一次查詢時,id=3的trx_id=14大于max_trx_id,說明是生成ReadView后才生成的記錄,不可以訪問到;
  4. session1的update操作,session1事務(wù)生成trx_id=15,因?yàn)楦耰d=3,所以id=3的Record也是trx_id=15;
  5. session1的第二次查詢時,id=3的trx_id=15等于creator_trx_id(自己的事務(wù)id),可以被訪問到;

因此再次出現(xiàn)了幻讀。

好文閱讀

庖丁解InnoDB之Undo LOG

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容