MySQL中的MVCC底層原理解讀
簡(jiǎn)介
MVCC(Multi-Version Concurrency Control)多版本并發(fā)控制,是用來(lái)在數(shù)據(jù)庫(kù)中控制并發(fā)的方法,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的并發(fā)訪(fǎng)問(wèn)用的。
在MySQL中,MVCC只在讀取已提交(Read Committed)和可重復(fù)讀(Repeatable Read)兩個(gè)事務(wù)級(jí)別下有效。其是通過(guò)Undo日志中的版本鏈和ReadView一致性視圖來(lái)實(shí)現(xiàn)的。
MVCC就是在多個(gè)事務(wù)同時(shí)存在時(shí),SELECT語(yǔ)句找尋到具體是版本鏈上的哪個(gè)版本,然后在找到的版本上返回其中所記錄的數(shù)據(jù)的過(guò)程。
首先需要知道的是,在MySQL中,會(huì)默認(rèn)為我們的表后面添加三個(gè)隱藏字段:
DB_ROW_ID
:行ID,MySQL的B+樹(shù)索引特性要求每個(gè)表必須要有一個(gè)主鍵。如果沒(méi)有設(shè)置的話(huà),會(huì)自動(dòng)尋找第一個(gè)不包含NULL的唯一索引列作為主鍵。如果還是找不到,就會(huì)在這個(gè)DB_ROW_ID上自動(dòng)生成一個(gè)唯一值,以此來(lái)當(dāng)作主鍵(該列和MVCC的關(guān)系不大);DB_TRX_ID
:事務(wù)ID,記錄的是當(dāng)前事務(wù)在做INSERT或UPDATE語(yǔ)句操作時(shí)的事務(wù)ID(DELETE語(yǔ)句被當(dāng)做是UPDATE語(yǔ)句的特殊情況,后面會(huì)進(jìn)行說(shuō)明);DB_ROLL_PTR
:回滾指針,通過(guò)它可以將不同的版本串聯(lián)起來(lái),形成版本鏈。相當(dāng)于鏈表的next指針。
(注意,添加的隱藏字段并不是很多人認(rèn)為的創(chuàng)建時(shí)間和刪除時(shí)間,同時(shí)在MySQL中MVCC的實(shí)現(xiàn)也不是通過(guò)什么快照來(lái)實(shí)現(xiàn)的。之所以有這種說(shuō)法可能是源自于《高性能MySQL》一書(shū)中對(duì)MySQL中MVCC的錯(cuò)誤結(jié)論,然后就人云亦云傳開(kāi)了(注意,我這里一直強(qiáng)調(diào)的是MySQL中MVCC的實(shí)現(xiàn),是因?yàn)樵诓煌臄?shù)據(jù)庫(kù)中可能會(huì)有不同的實(shí)現(xiàn))。所以說(shuō)看源碼和看官方文檔才是最權(quán)威的解釋?zhuān)?/p>
ReadView
ReadView一致性視圖主要是由兩部分組成:所有未提交事務(wù)的ID數(shù)組和已經(jīng)創(chuàng)建的最大事務(wù)ID組成(實(shí)際上ReadView還有其他的字段,但不影響這里對(duì)MVCC的講解)。
比如:[100,200],300。事務(wù)100和200是當(dāng)前未提交的事務(wù),而事務(wù)300是當(dāng)前創(chuàng)建的最大事務(wù)(已經(jīng)提交了)。
當(dāng)執(zhí)行SELECT語(yǔ)句的時(shí)候會(huì)創(chuàng)建ReadView,但是在讀取已提交和可重復(fù)讀兩個(gè)事務(wù)級(jí)別下,生成ReadView的策略是不一樣的:讀取已提交級(jí)別是每執(zhí)行一次SELECT語(yǔ)句就會(huì)重新生成一份ReadView,而可重復(fù)讀級(jí)別是只會(huì)在第一次SELECT語(yǔ)句執(zhí)行的時(shí)候會(huì)生成一份,后續(xù)的SELECT語(yǔ)句會(huì)沿用之前生成的ReadView(即使后面有更新語(yǔ)句的話(huà),也會(huì)繼續(xù)沿用)。
版本鏈
所有版本的數(shù)據(jù)都只會(huì)存一份,然后通過(guò)回滾指針連接起來(lái),之后就是通過(guò)一定的規(guī)則找到具體是哪個(gè)版本上的數(shù)據(jù)就行了。
假設(shè)現(xiàn)在有一張account表,其中有id和name兩個(gè)字段,那么版本鏈的示意圖如下:
而具體版本鏈的比對(duì)規(guī)則如下,首先從版本鏈中拿出最上面第一個(gè)版本的事務(wù)ID開(kāi)始逐個(gè)往下進(jìn)行比對(duì):
(其中min_id指向ReadView中未提交事務(wù)數(shù)組中的最小事務(wù)ID,而max_id指向ReadView中的已經(jīng)創(chuàng)建的最大事務(wù)ID)
如果落在綠色區(qū)間(DB_TRX_ID < min_id):這個(gè)版本比min_id還?。ㄊ聞?wù)ID是從小往大順序生成的),說(shuō)明這個(gè)版本在SELECT之前就已經(jīng)提交了,所以這個(gè)數(shù)據(jù)是可見(jiàn)的?;蛘撸ㄟ@里是短路或,前面條件不滿(mǎn)足才會(huì)判斷后面這個(gè)條件)這個(gè)版本的事務(wù)本身就是當(dāng)前SELECT語(yǔ)句所在事務(wù)的話(huà),也是一樣可見(jiàn)的;
如果落在紅色區(qū)間(DB_TRX_ID > max_id):表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)來(lái)生成的,當(dāng)前還未開(kāi)始,那么是不可見(jiàn)的;
如果落在黃色區(qū)間(min_id <= DB_TRX_ID <= max_id):這個(gè)時(shí)候就需要再判斷兩種情況:
- 如果這個(gè)版本的事務(wù)ID在ReadView的未提交事務(wù)數(shù)組中,表示這個(gè)版本是由還未提交的事務(wù)生成的,那么就是不可見(jiàn)的;
- 如果這個(gè)版本的事務(wù)ID不在ReadView的未提交事務(wù)數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,那么是可見(jiàn)的。
如果在上述的判斷中發(fā)現(xiàn)當(dāng)前版本是不可見(jiàn)的,那么就繼續(xù)從版本鏈中通過(guò)回滾指針拿取下一個(gè)版本來(lái)進(jìn)行上述的判斷。
演示過(guò)程
下面通過(guò)一個(gè)示例來(lái)具體演示MVCC的執(zhí)行過(guò)程(假設(shè)是在可重復(fù)讀事務(wù)級(jí)別下),當(dāng)前account表中已經(jīng)有了一條初始數(shù)據(jù)(id=1,name=monkey):
Transaction 100 | Transaction 200 | Transaction 300 | 無(wú)事務(wù)ID | 無(wú)事務(wù)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; |
從左往右分別是五個(gè)事務(wù),從上到下是時(shí)刻點(diǎn)。其中在第2和3時(shí)刻點(diǎn)中事務(wù)100和事務(wù)200(這里兩個(gè)事務(wù)之間相差100只是為了更加方便去看,正常來(lái)說(shuō)下個(gè)事務(wù)的ID是以+1的方式來(lái)創(chuàng)建的)分別執(zhí)行了一條UPDATE語(yǔ)句,這兩條語(yǔ)句并無(wú)實(shí)際作用,只是為了生成事務(wù)ID的,所以在下面的MVCC執(zhí)行過(guò)程中就不分析這兩條語(yǔ)句所帶來(lái)的影響了,我們只研究account表。而其中最后兩個(gè)事務(wù),我是注明沒(méi)有事務(wù)ID的。因?yàn)槭聞?wù)ID是執(zhí)行一條更新操作(增刪改)的語(yǔ)句后才會(huì)生成(這也是事務(wù)100和事務(wù)200要先執(zhí)行一條更新語(yǔ)句的意義),并不是開(kāi)啟事務(wù)的時(shí)候就會(huì)生成。最后兩個(gè)事務(wù)中可以看到就是執(zhí)行了一些SELECT語(yǔ)句而已,所以它們并沒(méi)有事務(wù)ID。
首先來(lái)看一下初始狀態(tài)時(shí)的版本鏈和ReadView(ReadView此時(shí)還未生成):
其中事務(wù)1在account表中創(chuàng)建了一條初始數(shù)據(jù)。
- 之后在第1時(shí)刻點(diǎn),五個(gè)事務(wù)分別開(kāi)啟了事務(wù)(如上所說(shuō),這個(gè)時(shí)候還沒(méi)有生成事務(wù)ID)。
- 在第2時(shí)刻點(diǎn),第一個(gè)事務(wù)執(zhí)行了一條UPDATE語(yǔ)句,生成了事務(wù)ID為100。
- 在第3時(shí)刻點(diǎn),第二個(gè)事務(wù)執(zhí)行了一條UPDATE語(yǔ)句,生成了事務(wù)ID為200。
- 在第4時(shí)刻點(diǎn),第三個(gè)事務(wù)執(zhí)行了一條UPDATE語(yǔ)句,將account表中id為1的name改為了monkey301。同時(shí)生成了事務(wù)ID為300。
- 在第5時(shí)刻點(diǎn),事務(wù)300也就是上面的事務(wù)執(zhí)行了commit操作。
- 在第6時(shí)刻點(diǎn),第四個(gè)事務(wù)執(zhí)行了一條SELECT語(yǔ)句,想要查詢(xún)一下當(dāng)前id為1的數(shù)據(jù)(如上所說(shuō),該事務(wù)沒(méi)有生成事務(wù)ID)。
此時(shí)的版本鏈和ReadView如下:
因?yàn)樵诘?時(shí)刻點(diǎn),事務(wù)300已經(jīng)commit了,所以ReadView的未提交事務(wù)數(shù)組中不包含它。此時(shí)根據(jù)上面所說(shuō)的比對(duì)規(guī)則,拿版本鏈中的第一個(gè)版本的事務(wù)ID為300進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)300中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務(wù)300也沒(méi)有在ReadView的未提交事務(wù)數(shù)組中,所以是可見(jiàn)的。即此時(shí)在第6時(shí)刻點(diǎn),第四個(gè)事務(wù)所查找到的結(jié)果是monkey301。
- 在第7時(shí)刻點(diǎn),事務(wù)100執(zhí)行了一條UPDATE語(yǔ)句,將account表中id為1的name改為了monkey101。
- 在第8時(shí)刻點(diǎn),事務(wù)100又執(zhí)行了一條UPDATE語(yǔ)句,將account表中id為1的name改為了monkey102。
- 在第9時(shí)刻點(diǎn),第四個(gè)事務(wù)執(zhí)行了一條SELECT語(yǔ)句,想要查詢(xún)一下當(dāng)前id為1的數(shù)據(jù)。
此時(shí)的版本鏈和ReadView如下:
注意,因?yàn)楫?dāng)前是在可重復(fù)讀的事務(wù)級(jí)別下,所以此時(shí)的ReadView沿用了在第6時(shí)刻點(diǎn)生成的ReadView(如果是在讀取已提交的事務(wù)級(jí)別下,此時(shí)就會(huì)重新生成一份ReadView了)。然后根據(jù)上面所說(shuō)的比對(duì)規(guī)則,拿版本鏈中的第一個(gè)版本的事務(wù)ID為100進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)100中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務(wù)100是在ReadView的未提交事務(wù)數(shù)組中,所以是不可見(jiàn)的。此時(shí)通過(guò)回滾指針拿取下一個(gè)版本,發(fā)現(xiàn)事務(wù)ID仍然為100,經(jīng)過(guò)分析后還是不可見(jiàn)的。此時(shí)又拿取下一個(gè)版本:事務(wù)ID為300進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)300中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,但是事務(wù)300沒(méi)有在ReadView的未提交事務(wù)數(shù)組中,所以是可見(jiàn)的。即此時(shí)在第9時(shí)刻點(diǎn),第四個(gè)事務(wù)所查找到的結(jié)果仍然是monkey301(這也就是可重復(fù)讀的含義)。
- 在第10時(shí)刻點(diǎn),事務(wù)100commit提交事務(wù)了。同時(shí)事務(wù)200執(zhí)行了一條UPDATE語(yǔ)句,將account表中id為1的name改為了monkey201。
- 在第11時(shí)刻點(diǎn),事務(wù)200又執(zhí)行了一條UPDATE語(yǔ)句,將account表中id為1的name改為了monkey202。
- 在第12時(shí)刻點(diǎn),第四個(gè)事務(wù)執(zhí)行了一條SELECT語(yǔ)句,想要查詢(xún)一下當(dāng)前id為1的數(shù)據(jù)。
此時(shí)的版本鏈和ReadView如下:
跟第9時(shí)刻點(diǎn)一樣,在可重復(fù)讀的事務(wù)級(jí)別下,ReadView沿用了在第6時(shí)刻點(diǎn)生成的ReadView。然后根據(jù)上面所說(shuō)的比對(duì)規(guī)則,拿版本鏈中的第一個(gè)版本的事務(wù)ID為200進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)200中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務(wù)200是在ReadView的未提交事務(wù)數(shù)組中,所以是不可見(jiàn)的。此時(shí)通過(guò)回滾指針拿取下一個(gè)版本,發(fā)現(xiàn)事務(wù)ID仍然為200,經(jīng)過(guò)分析后還是不可見(jiàn)的。此時(shí)又拿取下一個(gè)版本:事務(wù)ID為100進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)100中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間內(nèi),同時(shí)在ReadView的未提交數(shù)組中,所以依然是不可見(jiàn)的。此時(shí)又拿取下一個(gè)版本,發(fā)現(xiàn)事務(wù)ID仍然為100,經(jīng)過(guò)分析后還是不可見(jiàn)的。此時(shí)再拿取下一個(gè)版本:事務(wù)ID為300進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)300中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,但是事務(wù)300沒(méi)有在ReadView的未提交事務(wù)數(shù)組中,所以是可見(jiàn)的。即此時(shí)在第12時(shí)刻點(diǎn),第四個(gè)事務(wù)所查找到的結(jié)果仍然是monkey301。
同時(shí)在第12時(shí)刻點(diǎn),第五個(gè)事務(wù)執(zhí)行了一條SELECT語(yǔ)句,想要查詢(xún)一下當(dāng)前id為1的數(shù)據(jù)。
此時(shí)的版本鏈和ReadView如下:
注意,此時(shí)第五個(gè)事務(wù)因?yàn)槭窃撌聞?wù)內(nèi)的第一條SELECT語(yǔ)句,所以會(huì)重新生成在當(dāng)前情況下的ReadView,即上圖中所示的內(nèi)容??梢钥吹剑偷谒膫€(gè)事務(wù)生成的ReadView并不一樣,因?yàn)樵谥暗牡?0時(shí)刻點(diǎn),事務(wù)100已經(jīng)提交事務(wù)了。然后根據(jù)上面所說(shuō)的比對(duì)規(guī)則,拿版本鏈中的第一個(gè)版本的事務(wù)ID為200進(jìn)行比對(duì),首先當(dāng)前這條SELECT語(yǔ)句沒(méi)有在事務(wù)200中進(jìn)行查詢(xún),然后發(fā)現(xiàn)是落在黃色區(qū)間,而且事務(wù)200是在ReadView的未提交事務(wù)數(shù)組中,所以是不可見(jiàn)的。此時(shí)通過(guò)回滾指針拿取下一個(gè)版本,發(fā)現(xiàn)事務(wù)ID仍然為200,經(jīng)過(guò)分析后還是不可見(jiàn)的。此時(shí)又拿取下一個(gè)版本:事務(wù)ID為100進(jìn)行比對(duì),發(fā)現(xiàn)是在綠色區(qū)間,所以是可見(jiàn)的。即此時(shí)在第12時(shí)刻點(diǎn),第五個(gè)事務(wù)所查找到的結(jié)果是monkey102(可以看到,即使是同一條SELECT語(yǔ)句,在不同的事務(wù)中,查詢(xún)出來(lái)的結(jié)果也可能是不同的,究其原因就是因?yàn)镽eadView的不同)。
- 在第13時(shí)刻點(diǎn),事務(wù)200執(zhí)行了commit操作,整段分析過(guò)程結(jié)束。
以上演示的就是MVCC的具體執(zhí)行過(guò)程,在多個(gè)事務(wù)下,版本鏈和ReadView是如何配合進(jìn)行查找的。上面還遺漏了一種情況沒(méi)有進(jìn)行說(shuō)明,就是如果是DELETE語(yǔ)句的話(huà),也會(huì)在版本鏈上將最新的數(shù)據(jù)插入一份,然后將事務(wù)ID賦值為當(dāng)前進(jìn)行刪除操作的事務(wù)ID。但是同時(shí)會(huì)在該條記錄的信息頭(record header)里面的deleted_flag標(biāo)記位置為true,以此來(lái)表示當(dāng)前記錄已經(jīng)被刪除。所以如果經(jīng)過(guò)版本比對(duì)后發(fā)現(xiàn)找到的版本上的deleted_flag標(biāo)記位為true的話(huà),那么也不會(huì)返回,而是繼續(xù)尋找下一個(gè)。
另外,如果當(dāng)前事務(wù)執(zhí)行rollback回滾的話(huà),會(huì)把版本鏈中屬于該事務(wù)的所有版本都刪除掉。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
MySQL子查詢(xún)中order by不生效問(wèn)題的解決方法
ORDER BY 語(yǔ)句用于根據(jù)指定的列對(duì)結(jié)果集進(jìn)行排序,在日常工作中經(jīng)常會(huì)用到,這篇文章主要給大家介紹了關(guān)于MySQL子查詢(xún)中order by不生效問(wèn)題的解決方法,需要的朋友可以參考下2021-07-07學(xué)習(xí)mysql?如何行轉(zhuǎn)列與列傳行
這篇文章主要介紹了mysql行轉(zhuǎn)列與列傳行的使用方法,幫助大家更好的理解和學(xué)習(xí)MySQL的使用,語(yǔ)句不難,但有一定的知識(shí)參考價(jià)值,需要的朋友可以參考一下,希望給你的學(xué)習(xí)帶來(lái)幫助2022-02-02MySQL優(yōu)化總結(jié)-查詢(xún)總條數(shù)
這篇文章主要介紹了MySQL優(yōu)化總結(jié)-查詢(xún)總條數(shù)的相關(guān)內(nèi)容,文中進(jìn)行簡(jiǎn)單的測(cè)試對(duì)比,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10解決mysql導(dǎo)入還原時(shí)亂碼的問(wèn)題
sql文件,直接記事本方式打開(kāi),中文顯示正常,還原導(dǎo)入后,發(fā)現(xiàn)中文是亂碼2012-12-12