MySQL MVVC多版本并發(fā)控制的實(shí)現(xiàn)詳解
一、概述
MVCC(Multiversion Concurrency Control),多版本并發(fā)控制。它和undo log中的版本鏈息息相關(guān),MVVC通過數(shù)據(jù)行的多個(gè)版本來實(shí)現(xiàn)數(shù)據(jù)庫的并發(fā)控制。
簡(jiǎn)單的說就是當(dāng)前事務(wù)查詢另一個(gè)事務(wù)正在更改的行(如果此時(shí)讀取就會(huì)發(fā)生臟讀),不用加鎖等待,而是讀取該數(shù)據(jù)的歷史版本,降低響應(yīng)時(shí)間。
MVVC是通過undo log和Read View兩種技術(shù)實(shí)現(xiàn)的。
二、快照讀與當(dāng)前讀
MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀 ,而這個(gè)讀指的就是快照讀 , 而非當(dāng)前讀。當(dāng)前讀實(shí)際上是一種加鎖的操作。
1.當(dāng)前讀
當(dāng)前讀讀取的記錄一定是最新的數(shù)據(jù),讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(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ù)的隔離級(jí)別是串行時(shí),快照讀就沒有用了,會(huì)退化為當(dāng)前讀。
三、隔離級(jí)別與版本鏈復(fù)習(xí)
隔離級(jí)別:
在MySQL中默認(rèn)的隔離級(jí)別就是可重復(fù)讀RR,可以解決不可重復(fù)讀問題,在MySQL中,特別的還額外支持解決幻讀問題。
它是如何解決幻讀問題的呢?有兩種方式:
- 使用間隙鎖和臨鍵鎖解決,簡(jiǎn)而言之就是加鎖,在此期間其他事務(wù)不能夠插入數(shù)據(jù)
- MVCC方式,無需加鎖,消耗低(缺點(diǎn)是沒有完全解決幻讀問題)。
undo log版本鏈:
對(duì)應(yīng)InnoDB來說,聚簇索引中的每個(gè)記錄都包含了兩個(gè)必要的隱藏字段:
- trx_id:每次一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù)id賦值給trx_id隱藏列。
- roll_pointer:回滾指針,每次修改數(shù)據(jù)時(shí),都會(huì)把舊數(shù)據(jù)放入undo log日志中,新的數(shù)據(jù)指向該舊數(shù)據(jù),做成一個(gè)版本鏈,該指針字段就稱為回滾指針,通過該指針可以找到修改前的數(shù)據(jù)。
舉例:
有一個(gè)id為8的事務(wù)創(chuàng)建了一條數(shù)據(jù),那么該記錄的示意圖大概如下:
假設(shè)之后兩個(gè)id分別為10、20的事務(wù)對(duì)這條記錄進(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; |
每次修改都會(huì)生成一個(gè)undo log日志,每個(gè)日志都相互鏈接,構(gòu)成版本鏈,此時(shí)該條數(shù)據(jù)的示意圖如下:
每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù)id 。
四、Read View
有了undo log就可以讀取到記錄的歷史版本,那么在什么情況下,讀取哪個(gè)版本的記錄呢?這就用到了Read View,它幫我們解決了行的可見性問題。
Read View就是當(dāng)某個(gè)事務(wù)在使用MVVC機(jī)制進(jìn)行快照讀操作時(shí)產(chǎn)生的讀視圖。該視圖是數(shù)據(jù)庫當(dāng)前所有活躍事務(wù)id(還未提交的事務(wù))組成的列表的一個(gè)快照。
1.實(shí)現(xiàn)原理
四種隔離級(jí)別里,讀未提交和串行化是不會(huì)使用MVVC的,因?yàn)樽x未提交直接讀取某個(gè)數(shù)據(jù)的最新數(shù)據(jù)即可,串行化是通過加鎖來讀的。
讀已提交和可重復(fù)讀都必須保證讀到的數(shù)據(jù)都是其他事務(wù)提交了的,所以,其他事務(wù)修改了數(shù)據(jù)但是還未提交,我們不能夠訪問該數(shù)據(jù),但可以通過MVVC機(jī)制讀取該記錄的歷史版本,核心問題就是需要判斷版本鏈中的哪條歷史版本是當(dāng)前事務(wù)可見的,這也是ReadView要解決的問題。
Read View包含4個(gè)比較重要的內(nèi)容:
- creator_trx_id:創(chuàng)建這個(gè)Read View的事務(wù)id,Read View和事務(wù)是一一對(duì)應(yīng)的。
只有事務(wù)對(duì)表中的記錄做修改時(shí)才會(huì)為事務(wù)分配事務(wù)id,否則一個(gè)事務(wù)中只有讀操作,該事務(wù)的id默認(rèn)為0。
- trx_ids:表示在生成Read View時(shí)當(dāng)前系統(tǒng)中活躍的事務(wù)id列表。提交了的事務(wù)不在其中。
- up_limit_id:活躍的事務(wù)中最小的事務(wù)id。
- low_limit_id:表示生成Read View時(shí)系統(tǒng)應(yīng)該分配給下一個(gè)事務(wù)的id值,同樣也表示系統(tǒng)中最大的事務(wù)id值。
注意:low_limit_id并不是trx_ids中的最大值,事務(wù)id是遞增分配的。比如,現(xiàn)在有id為1, 2,5這三個(gè)事務(wù),之后id為5的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成ReadView時(shí), trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是6。
2.Read View規(guī)則
版本鏈
當(dāng)某個(gè)事務(wù)有了Read View,訪問某條記錄時(shí),需要按照下面的步驟判斷該記錄的哪個(gè)版本可見:
- 如果該版本記錄的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時(shí),此事務(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)查詢一條記錄的時(shí)候,系統(tǒng)如何通過MVCC找到它:
- 首先獲取事務(wù)自己的版本號(hào),也就是事務(wù)ID;
- 獲取 ReadView;
- 查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號(hào)進(jìn)行比較;
- 如果不符合 ReadView 規(guī)則,就需要從Undo Log中獲取歷史快照;
- 最后返回符合規(guī)則的數(shù)據(jù)。
在隔離級(jí)別為讀已提交時(shí),一個(gè)事務(wù)中的每一次SELECT查詢都會(huì)重新獲取一次Read View,而可重復(fù)讀是第一SELECT操作才會(huì)生成Read View,之后的查詢操作復(fù)用這一個(gè)。
導(dǎo)致這兩種的差距是因?yàn)椋嚎芍貜?fù)讀要保證一個(gè)事務(wù)中相同的SELECT讀取的內(nèi)容是相同的。
五、舉例
1.READ
COMMITTED隔離級(jí)別下
現(xiàn)在有兩個(gè)事務(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的記錄得到的版本鏈表如下所示:
此時(shí)新來一個(gè)事務(wù)執(zhí)行如下操作:
begin; select * from t where id=1; -- 事務(wù)10、20未提交
查詢到的結(jié)果為張三。
具體的過程如下:
- 在執(zhí)行select語句前,先生成一個(gè)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)行對(duì)比,因?yàn)閠rx_id為10,10剛好是trx_ids中的記錄,所以這條記錄對(duì)當(dāng)前事務(wù)不可見,根據(jù)回滾指針得到下一個(gè)版本
- 下一個(gè)版本name為李四,也不行
- 繼續(xù)找到name為張三的版本,trx_id為8,8小于up_limit_id,所以該版本對(duì)當(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;
此時(shí)版本鏈更新為:
再到剛才使用READ COMMITTED隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)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)榍皟蓚€(gè)版本的記錄trx_id為20,存在trx_ids中,所以跳過
- 到第三條記錄時(shí),trx_id為10,小于20,可以讀取,所以最終結(jié)果為王五
注意:READ COMMITTED,每次讀取數(shù)據(jù)前都生成一個(gè)新的ReadView。
2.REPEATABLE READ隔離級(jí)別下
假如此時(shí)id為10的事務(wù)和id為20的事務(wù)正在修改,都未提交,修改內(nèi)容和前面的一樣,但是還未提交,此時(shí)當(dāng)前事務(wù)做一個(gè)查詢。
步驟為:
- 生成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ù)
此時(shí),id為10的記錄提交事務(wù)。
當(dāng)前事務(wù)又需要select id為1的記錄,步驟為:
- 因?yàn)槭强芍貜?fù)讀,且第一次select已經(jīng)生成過Read View了,所有會(huì)復(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。
此時(shí)當(dāng)前事務(wù)啟動(dòng)了,執(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ù)只有一個(gè),且該數(shù)據(jù)的trx_id為8,小于up_limit_id,所以可以讀取到。
在這之后id為10的事務(wù)新增了一行數(shù)據(jù),增加了id為2的數(shù)據(jù),且提交了。
此時(shí)當(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中存在,所以該記錄對(duì)當(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
- 本文我們主要介紹了MySQL性能分析以及explain的使用,包括:組合索引、慢查詢分析、MYISAM和INNODB的鎖定、MYSQL的事務(wù)配置項(xiàng)等,希望能夠?qū)δ兴鶐椭?/div> 2011-08-08
MYSQL實(shí)現(xiàn)添加購物車時(shí)防止重復(fù)添加示例代碼
在向mysql中插入數(shù)據(jù)的時(shí)候最需要注意的就是防止重復(fù)發(fā)添加數(shù)據(jù),下面這篇文章主要給大家介紹了關(guān)于MYSQL如何實(shí)現(xiàn)添加購物車的時(shí)候防止重復(fù)添加的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-09-09MySQL出現(xiàn)Waiting for table metadata lock的原因方法
在本篇內(nèi)容里小編給大家整理了MySQL出現(xiàn)Waiting for table metadata lock的原因以及解決方法對(duì)此有需要的朋友們學(xué)習(xí)下。2019-05-05MySQL數(shù)據(jù)庫表的合并及分區(qū)方式
這篇文章主要介紹了MySQL數(shù)據(jù)庫表的合并及分區(qū)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08最新評(píng)論