MySQL MVVC多版本并發(fā)控制的實(shí)現(xiàn)詳解
一、概述
MVCC(Multiversion Concurrency Control),多版本并發(fā)控制。它和undo log中的版本鏈息息相關(guān),MVVC通過數(shù)據(jù)行的多個版本來實(shí)現(xiàn)數(shù)據(jù)庫的并發(fā)控制。
簡單的說就是當(dāng)前事務(wù)查詢另一個事務(wù)正在更改的行(如果此時讀取就會發(fā)生臟讀),不用加鎖等待,而是讀取該數(shù)據(jù)的歷史版本,降低響應(yīng)時間。
MVVC是通過undo log和Read View兩種技術(shù)實(shí)現(xiàn)的。
二、快照讀與當(dāng)前讀
MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發(fā)讀 ,而這個讀指的就是快照讀 , 而非當(dāng)前讀。當(dāng)前讀實(shí)際上是一種加鎖的操作。
1.當(dāng)前讀
當(dāng)前讀讀取的記錄一定是最新的數(shù)據(jù),讀取時還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會對讀取的記錄進(jìn)行加鎖。
加鎖的讀被稱為當(dāng)前讀,還有數(shù)據(jù)的增刪改都是要先讀取數(shù)據(jù)的,這一讀取過程也是當(dāng)前讀。
SELECT * FROM t LOCK IN SHARE MODE; # 共享鎖 SELECT * FROM t FOR UPDATE; # 排他鎖 UPDATE SET t..
2.快照讀
快照讀又叫一致性讀,讀取的是數(shù)據(jù)行的快照版本。在MySQL中,普通的select語句(不加for update或lock in share mode的select語句)默認(rèn)就是使用的快照讀,不加鎖。
SELECT * FROM table WHERE ...
之所以這樣,是因?yàn)榭煺兆x可以避免加鎖操作,降低開銷。
當(dāng)事務(wù)的隔離級別是串行時,快照讀就沒有用了,會退化為當(dāng)前讀。
三、隔離級別與版本鏈復(fù)習(xí)
隔離級別:
在MySQL中默認(rèn)的隔離級別就是可重復(fù)讀RR,可以解決不可重復(fù)讀問題,在MySQL中,特別的還額外支持解決幻讀問題。
它是如何解決幻讀問題的呢?有兩種方式:
- 使用間隙鎖和臨鍵鎖解決,簡而言之就是加鎖,在此期間其他事務(wù)不能夠插入數(shù)據(jù)
- MVCC方式,無需加鎖,消耗低(缺點(diǎn)是沒有完全解決幻讀問題)。
undo log版本鏈:
對應(yīng)InnoDB來說,聚簇索引中的每個記錄都包含了兩個必要的隱藏字段:
- trx_id:每次一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,都會把該事務(wù)的事務(wù)id賦值給trx_id隱藏列。
- roll_pointer:回滾指針,每次修改數(shù)據(jù)時,都會把舊數(shù)據(jù)放入undo log日志中,新的數(shù)據(jù)指向該舊數(shù)據(jù),做成一個版本鏈,該指針字段就稱為回滾指針,通過該指針可以找到修改前的數(shù)據(jù)。
舉例:
有一個id為8的事務(wù)創(chuàng)建了一條數(shù)據(jù),那么該記錄的示意圖大概如下:

假設(shè)之后兩個id分別為10、20的事務(wù)對這條記錄進(jìn)行update操作,流程如下:
| 事務(wù)10 | 事務(wù)20 |
|---|---|
| BEGIN; | |
| BEGIN; | |
| UPDATE student SET name='李四' WHERE id=1; | |
| UPDATE student SET name='王五' WHERE id=1; | |
| COMMIT; | |
| UPDATE student SET name='趙六' WHERE id=1; | |
| UPDATE student SET name='錢七' WHERE id=1; | |
| COMMIT; |
每次修改都會生成一個undo log日志,每個日志都相互鏈接,構(gòu)成版本鏈,此時該條數(shù)據(jù)的示意圖如下:

每個版本中還包含生成該版本時對應(yīng)的事務(wù)id 。
四、Read View
有了undo log就可以讀取到記錄的歷史版本,那么在什么情況下,讀取哪個版本的記錄呢?這就用到了Read View,它幫我們解決了行的可見性問題。
Read View就是當(dāng)某個事務(wù)在使用MVVC機(jī)制進(jìn)行快照讀操作時產(chǎn)生的讀視圖。該視圖是數(shù)據(jù)庫當(dāng)前所有活躍事務(wù)id(還未提交的事務(wù))組成的列表的一個快照。
1.實(shí)現(xiàn)原理
四種隔離級別里,讀未提交和串行化是不會使用MVVC的,因?yàn)樽x未提交直接讀取某個數(shù)據(jù)的最新數(shù)據(jù)即可,串行化是通過加鎖來讀的。
讀已提交和可重復(fù)讀都必須保證讀到的數(shù)據(jù)都是其他事務(wù)提交了的,所以,其他事務(wù)修改了數(shù)據(jù)但是還未提交,我們不能夠訪問該數(shù)據(jù),但可以通過MVVC機(jī)制讀取該記錄的歷史版本,核心問題就是需要判斷版本鏈中的哪條歷史版本是當(dāng)前事務(wù)可見的,這也是ReadView要解決的問題。
Read View包含4個比較重要的內(nèi)容:
- creator_trx_id:創(chuàng)建這個Read View的事務(wù)id,Read View和事務(wù)是一一對應(yīng)的。
只有事務(wù)對表中的記錄做修改時才會為事務(wù)分配事務(wù)id,否則一個事務(wù)中只有讀操作,該事務(wù)的id默認(rèn)為0。
- trx_ids:表示在生成Read View時當(dāng)前系統(tǒng)中活躍的事務(wù)id列表。提交了的事務(wù)不在其中。
- up_limit_id:活躍的事務(wù)中最小的事務(wù)id。
- low_limit_id:表示生成Read View時系統(tǒng)應(yīng)該分配給下一個事務(wù)的id值,同樣也表示系統(tǒng)中最大的事務(wù)id值。
注意:low_limit_id并不是trx_ids中的最大值,事務(wù)id是遞增分配的。比如,現(xiàn)在有id為1, 2,5這三個事務(wù),之后id為5的事務(wù)提交了。那么一個新的讀事務(wù)在生成ReadView時, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是6。

2.Read View規(guī)則

版本鏈
當(dāng)某個事務(wù)有了Read View,訪問某條記錄時,需要按照下面的步驟判斷該記錄的哪個版本可見:
- 如果該版本記錄的trx_id和Read View的creator_trx_id相同,意味著該版本的記錄是由當(dāng)前事務(wù)修改的,因此該版本可以被當(dāng)前事務(wù)訪問
- 如果該版本記錄的trx_id小于Read View的up_limit_id,證明當(dāng)前事務(wù)生成Read View時,此事務(wù)已經(jīng)提交了,所以當(dāng)前事務(wù)可以讀取該版本。
- 如果該版本的trx_id大于等于low_limit_id,證明生成該版本的事務(wù)在當(dāng)前事務(wù)生成Read View之后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id屬性值在ReadView的up_limit_id和low_limit_id之間,那就需要判斷一下trx_id屬性值是不是在trx_ids列表中,如果不在的話才能訪問,否則不能訪問。
3.整體流程
了解了這些概念之后,我們來看下當(dāng)查詢一條記錄的時候,系統(tǒng)如何通過MVCC找到它:
- 首先獲取事務(wù)自己的版本號,也就是事務(wù)ID;
- 獲取 ReadView;
- 查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號進(jìn)行比較;
- 如果不符合 ReadView 規(guī)則,就需要從Undo Log中獲取歷史快照;
- 最后返回符合規(guī)則的數(shù)據(jù)。
在隔離級別為讀已提交時,一個事務(wù)中的每一次SELECT查詢都會重新獲取一次Read View,而可重復(fù)讀是第一SELECT操作才會生成Read View,之后的查詢操作復(fù)用這一個。
導(dǎo)致這兩種的差距是因?yàn)椋嚎芍貜?fù)讀要保證一個事務(wù)中相同的SELECT讀取的內(nèi)容是相同的。

五、舉例
1.READ
COMMITTED隔離級別下
現(xiàn)在有兩個事務(wù)id分別為10、20的事務(wù)在執(zhí)行:
-- id為10的事務(wù) begin; update t set name='李四' where id=1; update t set name='王五' where id=1; -- id為20的事務(wù) 更新其他行的數(shù)據(jù)
此刻,表中id為1的記錄得到的版本鏈表如下所示:

此時新來一個事務(wù)執(zhí)行如下操作:
begin; select * from t where id=1; -- 事務(wù)10、20未提交
查詢到的結(jié)果為張三。
具體的過程如下:
- 在執(zhí)行select語句前,先生成一個Read View,Read View的creator_trx_id為0,trx_ids列表的內(nèi)容是[10,20],up_limit_id為10,low_limit_id為21。
- 查詢name為王五的最新版本的記錄,按規(guī)則進(jìn)行對比,因?yàn)閠rx_id為10,10剛好是trx_ids中的記錄,所以這條記錄對當(dāng)前事務(wù)不可見,根據(jù)回滾指針得到下一個版本
- 下一個版本name為李四,也不行
- 繼續(xù)找到name為張三的版本,trx_id為8,8小于up_limit_id,所以該版本對當(dāng)前事務(wù)可見,得到最終結(jié)果
接下來,再將id為10的事務(wù)進(jìn)行commit提交。然后id為20的事務(wù)來更新記錄:
begin; -- id為20的事務(wù) update t set name='趙六' where id=1; update t set name='錢七' where id=1;
此時版本鏈更新為:

再到剛才使用READ COMMITTED隔離級別的事務(wù)中繼續(xù)查找這個id 為1的記錄,得到的結(jié)果為name=王五的那條記錄。執(zhí)行過程如下:
- 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內(nèi)容是[20],up_limit_id為20,low_limit_id為21。
- 因?yàn)榍皟蓚€版本的記錄trx_id為20,存在trx_ids中,所以跳過
- 到第三條記錄時,trx_id為10,小于20,可以讀取,所以最終結(jié)果為王五
注意:READ COMMITTED,每次讀取數(shù)據(jù)前都生成一個新的ReadView。
2.REPEATABLE READ隔離級別下
假如此時id為10的事務(wù)和id為20的事務(wù)正在修改,都未提交,修改內(nèi)容和前面的一樣,但是還未提交,此時當(dāng)前事務(wù)做一個查詢。

步驟為:
- 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內(nèi)容是[10,20],up_limit_id為10,low_limit_id為21。
- trx_id為10和20的都不滿足要求
- 最后查找到name為張三的歷史版本的數(shù)據(jù)
此時,id為10的記錄提交事務(wù)。

當(dāng)前事務(wù)又需要select id為1的記錄,步驟為:
- 因?yàn)槭强芍貜?fù)讀,且第一次select已經(jīng)生成過Read View了,所有會復(fù)用它,不重新生成。
- 所以trx_id為10和20的記錄依舊不符合規(guī)則,最終得到的數(shù)據(jù)還是張三,符合可重復(fù)讀的規(guī)范
注意:REPEATABLE READ,每次讀取都復(fù)用第一次生成的Read View
3.如何解決幻讀
假設(shè)現(xiàn)在有一條數(shù)據(jù),id為1

當(dāng)前活躍的事務(wù)有10和20。
此時當(dāng)前事務(wù)啟動了,執(zhí)行如下SQL語句:
begin; select * from student where id>=1;
在開始前生成Read View,內(nèi)容如下:creator_trx_id=0,trx_ids= [10,20] , up_limit_id=10, low_limit_id=21。
由于id大于等于1的數(shù)據(jù)只有一個,且該數(shù)據(jù)的trx_id為8,小于up_limit_id,所以可以讀取到。
在這之后id為10的事務(wù)新增了一行數(shù)據(jù),增加了id為2的數(shù)據(jù),且提交了。

此時當(dāng)前線程繼續(xù)查找id>=1的數(shù)據(jù),因?yàn)槭强芍貜?fù)讀,復(fù)用剛剛的Read View。
得到兩行數(shù)據(jù),但是因?yàn)閕d為2的數(shù)據(jù)trx_id為10,該值在Read View的trx_ids中存在,所以該記錄對當(dāng)前事務(wù)不可見,所以最后查詢到的數(shù)據(jù)只有一條記錄。
如果當(dāng)前事務(wù)再插入id為2的數(shù)據(jù)就插不進(jìn)去,所以說MVVC只解決了一半的幻讀問題。
到此這篇關(guān)于MySQL MVVC多版本并發(fā)控制的實(shí)現(xiàn)詳解的文章就介紹到這了,更多相關(guān)MySQL MVVC內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mysql 出現(xiàn)故障應(yīng)用直接中斷連接導(dǎo)致數(shù)據(jù)被鎖(生產(chǎn)故障)詳解
這篇文章主要介紹了 Mysql 出現(xiàn)故障應(yīng)用直接中斷連接導(dǎo)致數(shù)據(jù)被鎖(生產(chǎn)故障)詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01
數(shù)據(jù)庫Sql實(shí)現(xiàn)截取時間段和日期實(shí)例(SQL時間截取)
在許多情況下你也許只想得到日期和時間的一部分,而不是完整的日期和時間,下面這篇文章主要給大家介紹了關(guān)于數(shù)據(jù)庫Sql實(shí)現(xiàn)截取時間段和日期(SQL時間截取)的相關(guān)資料,需要的朋友可以參考下2023-05-05

