MySQL中的MVCC底層原理解讀
簡介
MVCC(Multi-Version Concurrency Control)多版本并發(fā)控制,是用來在數(shù)據(jù)庫中控制并發(fā)的方法,實現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問用的。
在MySQL中,MVCC只在讀取已提交(Read Committed)和可重復讀(Repeatable Read)兩個事務級別下有效。其是通過Undo日志中的版本鏈和ReadView一致性視圖來實現(xiàn)的。
MVCC就是在多個事務同時存在時,SELECT語句找尋到具體是版本鏈上的哪個版本,然后在找到的版本上返回其中所記錄的數(shù)據(jù)的過程。
首先需要知道的是,在MySQL中,會默認為我們的表后面添加三個隱藏字段:
DB_ROW_ID:行ID,MySQL的B+樹索引特性要求每個表必須要有一個主鍵。如果沒有設置的話,會自動尋找第一個不包含NULL的唯一索引列作為主鍵。如果還是找不到,就會在這個DB_ROW_ID上自動生成一個唯一值,以此來當作主鍵(該列和MVCC的關系不大);DB_TRX_ID:事務ID,記錄的是當前事務在做INSERT或UPDATE語句操作時的事務ID(DELETE語句被當做是UPDATE語句的特殊情況,后面會進行說明);DB_ROLL_PTR:回滾指針,通過它可以將不同的版本串聯(lián)起來,形成版本鏈。相當于鏈表的next指針。
(注意,添加的隱藏字段并不是很多人認為的創(chuàng)建時間和刪除時間,同時在MySQL中MVCC的實現(xiàn)也不是通過什么快照來實現(xiàn)的。之所以有這種說法可能是源自于《高性能MySQL》一書中對MySQL中MVCC的錯誤結論,然后就人云亦云傳開了(注意,我這里一直強調(diào)的是MySQL中MVCC的實現(xiàn),是因為在不同的數(shù)據(jù)庫中可能會有不同的實現(xiàn))。所以說看源碼和看官方文檔才是最權威的解釋)
ReadView
ReadView一致性視圖主要是由兩部分組成:所有未提交事務的ID數(shù)組和已經(jīng)創(chuàng)建的最大事務ID組成(實際上ReadView還有其他的字段,但不影響這里對MVCC的講解)。
比如:[100,200],300。事務100和200是當前未提交的事務,而事務300是當前創(chuàng)建的最大事務(已經(jīng)提交了)。
當執(zhí)行SELECT語句的時候會創(chuàng)建ReadView,但是在讀取已提交和可重復讀兩個事務級別下,生成ReadView的策略是不一樣的:讀取已提交級別是每執(zhí)行一次SELECT語句就會重新生成一份ReadView,而可重復讀級別是只會在第一次SELECT語句執(zhí)行的時候會生成一份,后續(xù)的SELECT語句會沿用之前生成的ReadView(即使后面有更新語句的話,也會繼續(xù)沿用)。
版本鏈
所有版本的數(shù)據(jù)都只會存一份,然后通過回滾指針連接起來,之后就是通過一定的規(guī)則找到具體是哪個版本上的數(shù)據(jù)就行了。
假設現(xiàn)在有一張account表,其中有id和name兩個字段,那么版本鏈的示意圖如下:

而具體版本鏈的比對規(guī)則如下,首先從版本鏈中拿出最上面第一個版本的事務ID開始逐個往下進行比對:

(其中min_id指向ReadView中未提交事務數(shù)組中的最小事務ID,而max_id指向ReadView中的已經(jīng)創(chuàng)建的最大事務ID)
如果落在綠色區(qū)間(DB_TRX_ID < min_id):這個版本比min_id還小(事務ID是從小往大順序生成的),說明這個版本在SELECT之前就已經(jīng)提交了,所以這個數(shù)據(jù)是可見的?;蛘撸ㄟ@里是短路或,前面條件不滿足才會判斷后面這個條件)這個版本的事務本身就是當前SELECT語句所在事務的話,也是一樣可見的;
如果落在紅色區(qū)間(DB_TRX_ID > max_id):表示這個版本是由將來啟動的事務來生成的,當前還未開始,那么是不可見的;
如果落在黃色區(qū)間(min_id <= DB_TRX_ID <= max_id):這個時候就需要再判斷兩種情況:
- 如果這個版本的事務ID在ReadView的未提交事務數(shù)組中,表示這個版本是由還未提交的事務生成的,那么就是不可見的;
- 如果這個版本的事務ID不在ReadView的未提交事務數(shù)組中,表示這個版本是已經(jīng)提交了的事務生成的,那么是可見的。
如果在上述的判斷中發(fā)現(xiàn)當前版本是不可見的,那么就繼續(xù)從版本鏈中通過回滾指針拿取下一個版本來進行上述的判斷。
演示過程
下面通過一個示例來具體演示MVCC的執(zhí)行過程(假設是在可重復讀事務級別下),當前account表中已經(jīng)有了一條初始數(shù)據(jù)(id=1,name=monkey):
| Transaction 100 | Transaction 200 | Transaction 300 | 無事務ID | 無事務ID | |
|---|---|---|---|---|---|
| 1 | begin; | begin; | begin; | begin; | begin; |
| 2 | UPDATE test SET a='1' WHERE id = 1; | ||||
| 3 | UPDATE test SET a='2' WHERE id = 2; | ||||
| 4 | UPDATE account SET name = 'monkey301' WHERE id = 1; | ||||
| 5 | commit; | ||||
| 6 | SELECT name FROM account WHERE id = 1; | ||||
| 7 | UPDATE account SET name = 'monkey101' WHERE id = 1; | ||||
| 8 | UPDATE account SET name = 'monkey102' WHERE id = 1; | ||||
| 9 | SELECT name FROM account WHERE id = 1; | ||||
| 10 | commit; | UPDATE account SET name = 'monkey201' WHERE id = 1; | |||
| 11 | UPDATE account SET name = 'monkey202' WHERE id = 1; | ||||
| 12 | SELECT name FROM account WHERE id = 1; | SELECT name FROM account WHERE id = 1; | |||
| 13 | commit; |
從左往右分別是五個事務,從上到下是時刻點。其中在第2和3時刻點中事務100和事務200(這里兩個事務之間相差100只是為了更加方便去看,正常來說下個事務的ID是以+1的方式來創(chuàng)建的)分別執(zhí)行了一條UPDATE語句,這兩條語句并無實際作用,只是為了生成事務ID的,所以在下面的MVCC執(zhí)行過程中就不分析這兩條語句所帶來的影響了,我們只研究account表。而其中最后兩個事務,我是注明沒有事務ID的。因為事務ID是執(zhí)行一條更新操作(增刪改)的語句后才會生成(這也是事務100和事務200要先執(zhí)行一條更新語句的意義),并不是開啟事務的時候就會生成。最后兩個事務中可以看到就是執(zhí)行了一些SELECT語句而已,所以它們并沒有事務ID。
首先來看一下初始狀態(tài)時的版本鏈和ReadView(ReadView此時還未生成):

其中事務1在account表中創(chuàng)建了一條初始數(shù)據(jù)。
- 之后在第1時刻點,五個事務分別開啟了事務(如上所說,這個時候還沒有生成事務ID)。
- 在第2時刻點,第一個事務執(zhí)行了一條UPDATE語句,生成了事務ID為100。
- 在第3時刻點,第二個事務執(zhí)行了一條UPDATE語句,生成了事務ID為200。
- 在第4時刻點,第三個事務執(zhí)行了一條UPDATE語句,將account表中id為1的name改為了monkey301。同時生成了事務ID為300。
- 在第5時刻點,事務300也就是上面的事務執(zhí)行了commit操作。
- 在第6時刻點,第四個事務執(zhí)行了一條SELECT語句,想要查詢一下當前id為1的數(shù)據(jù)(如上所說,該事務沒有生成事務ID)。
此時的版本鏈和ReadView如下:

因為在第5時刻點,事務300已經(jīng)commit了,所以ReadView的未提交事務數(shù)組中不包含它。此時根據(jù)上面所說的比對規(guī)則,拿版本鏈中的第一個版本的事務ID為300進行比對,首先當前這條SELECT語句沒有在事務300中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務300也沒有在ReadView的未提交事務數(shù)組中,所以是可見的。即此時在第6時刻點,第四個事務所查找到的結果是monkey301。
- 在第7時刻點,事務100執(zhí)行了一條UPDATE語句,將account表中id為1的name改為了monkey101。
- 在第8時刻點,事務100又執(zhí)行了一條UPDATE語句,將account表中id為1的name改為了monkey102。
- 在第9時刻點,第四個事務執(zhí)行了一條SELECT語句,想要查詢一下當前id為1的數(shù)據(jù)。
此時的版本鏈和ReadView如下:

注意,因為當前是在可重復讀的事務級別下,所以此時的ReadView沿用了在第6時刻點生成的ReadView(如果是在讀取已提交的事務級別下,此時就會重新生成一份ReadView了)。然后根據(jù)上面所說的比對規(guī)則,拿版本鏈中的第一個版本的事務ID為100進行比對,首先當前這條SELECT語句沒有在事務100中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務100是在ReadView的未提交事務數(shù)組中,所以是不可見的。此時通過回滾指針拿取下一個版本,發(fā)現(xiàn)事務ID仍然為100,經(jīng)過分析后還是不可見的。此時又拿取下一個版本:事務ID為300進行比對,首先當前這條SELECT語句沒有在事務300中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,但是事務300沒有在ReadView的未提交事務數(shù)組中,所以是可見的。即此時在第9時刻點,第四個事務所查找到的結果仍然是monkey301(這也就是可重復讀的含義)。
- 在第10時刻點,事務100commit提交事務了。同時事務200執(zhí)行了一條UPDATE語句,將account表中id為1的name改為了monkey201。
- 在第11時刻點,事務200又執(zhí)行了一條UPDATE語句,將account表中id為1的name改為了monkey202。
- 在第12時刻點,第四個事務執(zhí)行了一條SELECT語句,想要查詢一下當前id為1的數(shù)據(jù)。
此時的版本鏈和ReadView如下:

跟第9時刻點一樣,在可重復讀的事務級別下,ReadView沿用了在第6時刻點生成的ReadView。然后根據(jù)上面所說的比對規(guī)則,拿版本鏈中的第一個版本的事務ID為200進行比對,首先當前這條SELECT語句沒有在事務200中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務200是在ReadView的未提交事務數(shù)組中,所以是不可見的。此時通過回滾指針拿取下一個版本,發(fā)現(xiàn)事務ID仍然為200,經(jīng)過分析后還是不可見的。此時又拿取下一個版本:事務ID為100進行比對,首先當前這條SELECT語句沒有在事務100中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間內(nèi),同時在ReadView的未提交數(shù)組中,所以依然是不可見的。此時又拿取下一個版本,發(fā)現(xiàn)事務ID仍然為100,經(jīng)過分析后還是不可見的。此時再拿取下一個版本:事務ID為300進行比對,首先當前這條SELECT語句沒有在事務300中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,但是事務300沒有在ReadView的未提交事務數(shù)組中,所以是可見的。即此時在第12時刻點,第四個事務所查找到的結果仍然是monkey301。
同時在第12時刻點,第五個事務執(zhí)行了一條SELECT語句,想要查詢一下當前id為1的數(shù)據(jù)。
此時的版本鏈和ReadView如下:

注意,此時第五個事務因為是該事務內(nèi)的第一條SELECT語句,所以會重新生成在當前情況下的ReadView,即上圖中所示的內(nèi)容??梢钥吹?,和第四個事務生成的ReadView并不一樣,因為在之前的第10時刻點,事務100已經(jīng)提交事務了。然后根據(jù)上面所說的比對規(guī)則,拿版本鏈中的第一個版本的事務ID為200進行比對,首先當前這條SELECT語句沒有在事務200中進行查詢,然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務200是在ReadView的未提交事務數(shù)組中,所以是不可見的。此時通過回滾指針拿取下一個版本,發(fā)現(xiàn)事務ID仍然為200,經(jīng)過分析后還是不可見的。此時又拿取下一個版本:事務ID為100進行比對,發(fā)現(xiàn)是在綠色區(qū)間,所以是可見的。即此時在第12時刻點,第五個事務所查找到的結果是monkey102(可以看到,即使是同一條SELECT語句,在不同的事務中,查詢出來的結果也可能是不同的,究其原因就是因為ReadView的不同)。
- 在第13時刻點,事務200執(zhí)行了commit操作,整段分析過程結束。
以上演示的就是MVCC的具體執(zhí)行過程,在多個事務下,版本鏈和ReadView是如何配合進行查找的。上面還遺漏了一種情況沒有進行說明,就是如果是DELETE語句的話,也會在版本鏈上將最新的數(shù)據(jù)插入一份,然后將事務ID賦值為當前進行刪除操作的事務ID。但是同時會在該條記錄的信息頭(record header)里面的deleted_flag標記位置為true,以此來表示當前記錄已經(jīng)被刪除。所以如果經(jīng)過版本比對后發(fā)現(xiàn)找到的版本上的deleted_flag標記位為true的話,那么也不會返回,而是繼續(xù)尋找下一個。
另外,如果當前事務執(zhí)行rollback回滾的話,會把版本鏈中屬于該事務的所有版本都刪除掉。
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

