作者:京東零售??李澤陽(yáng)
最近在閱讀《認(rèn)知覺醒》這本書,里面有句話非常打動(dòng)我:通過(guò)自己的語(yǔ)言,用最簡(jiǎn)單的話把一件事情講清楚,最好讓外行人也能聽懂。
也許這就是大道至簡(jiǎn),只是我們習(xí)慣了煩瑣和復(fù)雜。
希望借助今天這篇文章,能用大白話說(shuō)清楚這個(gè)相對(duì)比較底層和復(fù)雜的MVCC機(jī)制。
在開始之前,先拋出一個(gè)問(wèn)題:我們都知道,目前(MySQL 5.6以上)數(shù)據(jù)庫(kù)已普遍使用InnoDB存儲(chǔ)引擎,InnoDB相對(duì)于MyISAM存儲(chǔ)引擎其中一個(gè)好處就是在數(shù)據(jù)庫(kù)級(jí)別鎖和表級(jí)別鎖的基礎(chǔ)上支持了行鎖,還有就是支持事務(wù),保證一組數(shù)據(jù)庫(kù)操作要么成功,要么失敗。基于此,問(wèn)題來(lái)了,在InnoDB默認(rèn)隔離級(jí)別(可重復(fù)讀)下,一個(gè)事務(wù)想要更新一行數(shù)據(jù),如果剛好有另外一個(gè)事務(wù)擁有這個(gè)行鎖,那么這個(gè)事務(wù)就會(huì)進(jìn)入等待狀態(tài)。既然進(jìn)入等待狀態(tài),那么等到這個(gè)事務(wù)獲取到行鎖要更新數(shù)據(jù)的時(shí)候,它讀取到的值是什么呢?
具體的問(wèn)題見下圖,我們?cè)O(shè)定有一張表user,初始化語(yǔ)句如下,試想在這樣的場(chǎng)景下,事務(wù)A三次查詢的值分別是什么?
create table `user` (
`id` bigint not null,
`name` varchar(50) default null,
PROMARY KEY (`id`)
) ENGINE = InnoDB;
insert into user(id,name) values (1,'A');
想要把這件事情回答正確,我們先來(lái)鋪墊一下基礎(chǔ)知識(shí)。
提到事務(wù),首先會(huì)想到的就是ACID(Atomic原子性、Consist一致性、Isolate隔離性、Durable持久性),今天我們主要關(guān)注隔離性,當(dāng)有多個(gè)事務(wù)同時(shí)執(zhí)行發(fā)生并發(fā)時(shí),數(shù)據(jù)庫(kù)可能會(huì)出現(xiàn)臟讀、不可重復(fù)讀和幻讀等問(wèn)題,為了解決這些問(wèn)題,“隔離級(jí)別”這位大哥上場(chǎng),包含:讀未提交、讀已提交、可重復(fù)讀和串行。
但我們都知道,隔離級(jí)別越高,執(zhí)行效率越低。畢竟大哥就是大哥,級(jí)別越高,越謹(jǐn)慎,常在河邊走哪能不濕鞋。
我們通過(guò)一個(gè)例子簡(jiǎn)單說(shuō)一下這四種隔離級(jí)別:
? 讀未提交:一個(gè)事務(wù)還未提交,它的變更就能被其他事務(wù)看到。V1為B,V2為B,V3為B。
? 讀已提交:一個(gè)事務(wù)提交之后,變更結(jié)果對(duì)其他事務(wù)可見。V1為A,V2和V3為B。
? 可重復(fù)讀:一個(gè)事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù)與事務(wù)啟動(dòng)時(shí)一致。V1為A,V2為A,V3為B。
? 串行:不管讀和寫,加鎖就完了,就是干!V1和V2均為A,V3為B。
事務(wù)是怎么實(shí)現(xiàn)的呢?實(shí)際上,事務(wù)執(zhí)行時(shí),數(shù)據(jù)庫(kù)會(huì)創(chuàng)建一個(gè)視圖,讀未提交直接返回最新值,沒(méi)有視圖概念;串行是直接加鎖避免并發(fā)訪問(wèn);讀已提交是在每個(gè)SQL語(yǔ)句開始執(zhí)行時(shí)創(chuàng)建的視圖??芍貜?fù)讀的視圖是在事務(wù)啟動(dòng)的時(shí)候創(chuàng)建的,整個(gè)事務(wù)都會(huì)使用這個(gè)視圖。這樣的話,上面四種不同隔離級(jí)別下的V1、V2、V3值便對(duì)號(hào)入座,有了結(jié)果。
MySQL是怎么實(shí)現(xiàn)的呢?我們以MySQL默認(rèn)的可重復(fù)讀隔離級(jí)別為例,實(shí)際上每條行記錄在更新時(shí)都會(huì)記錄一條回滾日志,也就是大家常說(shuō)的undo log。通過(guò)回滾操作,都可以得到前一個(gè)狀態(tài)的值。假設(shè)name值從初始值A(chǔ)被依次更新為B、C、D,我們看一下回滾日志:
當(dāng)前值是D,但是在查詢這條記錄的時(shí)候,不同時(shí)刻啟動(dòng)的事務(wù)會(huì)有不同的視圖,看到的值也就不一樣。在視圖1、2、3、4里面,記錄的name值分別是A、B、C、D。同一條行記錄在數(shù)據(jù)庫(kù)中可以存在多個(gè)版本,這就是多版本并發(fā)控制(MVCC)。對(duì)于視圖1,如果想要將name值回到A,那么就要依次執(zhí)行圖中所有回滾操作。
到這里,你已經(jīng)接觸到了MVCC的概念,也許你已經(jīng)對(duì)文章最開始的問(wèn)題有了一點(diǎn)點(diǎn)想法,別著急,我們先來(lái)簡(jiǎn)單總結(jié)下MVCC的特點(diǎn):
MVCC的出現(xiàn)使得一條行記錄在不同隔離級(jí)別下不同的事務(wù)操作會(huì)形成一條不同版本的鏈路,從而實(shí)現(xiàn)在不加鎖的前提下使不同事務(wù)的讀寫操作能夠并發(fā)安全執(zhí)行,這個(gè)版本鏈就是通過(guò)回滾日志undo log實(shí)現(xiàn)的。用大白話說(shuō),你這個(gè)事務(wù)想要查詢一條行記錄,MVCC會(huì)通過(guò)你這個(gè)事務(wù)所在視圖確認(rèn)版本鏈中哪個(gè)版本的行數(shù)據(jù)對(duì)你可見。剛才我們提到,四種隔離級(jí)別下,只有讀已提交和可重復(fù)讀會(huì)用到視圖。對(duì)于讀已提交,MVCC會(huì)在每次查詢前都會(huì)生成一個(gè)視圖,可重復(fù)讀隔離級(jí)別只會(huì)在第一次查詢時(shí)生成一個(gè)視圖,之后在這個(gè)事務(wù)中的所有查詢操作都會(huì)重復(fù)使用這個(gè)視圖。行業(yè)上,將創(chuàng)建視圖的那一刻稱為快照,晃你一下子,讓你激靈激靈,別發(fā)生臟讀,變臟嘍~
想要解決文章最開始的那個(gè)問(wèn)題,我們還得展開說(shuō)說(shuō)版本鏈?zhǔn)侨绾涡纬傻暮涂煺盏脑恚杂锌菰?,先忍一下,耐心看下去,乖?/p>
對(duì)于InnoDB存儲(chǔ)引擎來(lái)說(shuō),主鍵索引(也稱為聚簇索引)記錄中除了正常的字段數(shù)據(jù)外,還包含兩個(gè)隱藏列:
(1)trx_id:每次一個(gè)事務(wù)想要對(duì)主鍵索引進(jìn)行更新、刪除和新增時(shí),都會(huì)把這個(gè)事務(wù)的事務(wù)id賦值給trx_id字段。注意事務(wù)id嚴(yán)格遞增,且查詢操作不會(huì)分配事務(wù)id,即trx_id = 0;
(2)roll_point:每次一個(gè)事務(wù)對(duì)主鍵索引進(jìn)行更新時(shí),都會(huì)把舊的版本寫入到undo日志中,roll_point相當(dāng)于一個(gè)指針,通過(guò)它可以找到這條記錄修改前的信息。
我們以可重復(fù)讀隔離級(jí)別為例,為了尚未提交的更新結(jié)果對(duì)其他事務(wù)不可見,InnoDB在創(chuàng)建視圖時(shí),有以下四部分組成:
? m_ids:表示生成視圖時(shí),當(dāng)前系統(tǒng)中“活躍”的讀寫事務(wù)的事務(wù)id列表,這里的活躍大白話就是事務(wù)尚未提交;
? min_trx_id:表示在生成視圖時(shí),當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,即m_ids中的最小值;
? max_trx_id:表示生成視圖時(shí)系統(tǒng)應(yīng)該分配給下一個(gè)事務(wù)的id值;
? creator_trx_id:表示生成該視圖的事務(wù)id。
概念比較多,舉個(gè)例子,現(xiàn)在有事務(wù)id分別是1、2、3三個(gè)事務(wù),1和2事務(wù)尚未提交,3事務(wù)已提交,這個(gè)時(shí)候如果來(lái)了一個(gè)新事務(wù),那么它創(chuàng)建的視圖對(duì)應(yīng)這幾個(gè)參數(shù)分別為:m_ids包含1、2,min_trx_id為1,max_trx_id為4。
關(guān)鍵的知識(shí)點(diǎn)來(lái)了,如何根據(jù)某個(gè)事務(wù)生成的視圖,判斷版本鏈上的某個(gè)版本對(duì)這個(gè)事務(wù)可見呢?
遵循下面步驟:
1、版本鏈上的不同版本trx_id值如果與這個(gè)視圖的creator_trx_id值相同,說(shuō)明當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以被訪問(wèn)的版本對(duì)當(dāng)前事務(wù)可見。一家人還是認(rèn)識(shí)一家人的~
2、版本鏈上的不同版本trx_id值小于這個(gè)視圖的min_trx_id值,說(shuō)明這個(gè)版本的事務(wù)在當(dāng)前事務(wù)生成視圖之前就已經(jīng)提交了,所以被訪問(wèn)的版本對(duì)當(dāng)前事務(wù)可見。
3、版本鏈上的不同版本的trx_id值大于或等于這個(gè)視圖的max_trx_id值,說(shuō)明這個(gè)版本的事務(wù)在當(dāng)前事務(wù)之后才開啟,所以被訪問(wèn)版本對(duì)當(dāng)前事務(wù)不可見。
4、版本鏈上的不同版本的trx_id值在這個(gè)視圖的min_trx_id和max_trx_id之間,需要進(jìn)一步判斷被訪問(wèn)版本trx_id值是不是在m_ids中,如果在,說(shuō)明當(dāng)前事務(wù)是活躍的,被訪問(wèn)版本對(duì)當(dāng)前事務(wù)不可見。如果不在,說(shuō)明被訪問(wèn)版本的事務(wù)已經(jīng)提交了,被訪問(wèn)版本對(duì)當(dāng)前事務(wù)可見。
比較繞是不是,千萬(wàn)別暈,兄弟呀~,大白話解釋一下,設(shè)定某個(gè)事務(wù)生成的視圖瞬間(也就是快照),這個(gè)事務(wù)的id為creator_trx_id,那么有下面三種可能:
1、如果creator_trx_id落在綠色部分,表示被訪問(wèn)的版本是已提交的事務(wù)或者就是當(dāng)前事務(wù)自己生成的,這個(gè)數(shù)據(jù)是可見的;
2、如果creator_trx_id落在紅色部分,表示被訪問(wèn)的版本還未開啟,數(shù)據(jù)不可見;
3、如果creator_trx_id落在黃色部分,包括兩種情況:
若creator_trx_id在m_ids集合中,表示被訪問(wèn)的版本尚未提交,數(shù)據(jù)不可見;
若creator_trx_id不在m_ids集合中,表示被訪問(wèn)的版本已經(jīng)已經(jīng)提交了,數(shù)據(jù)可見。
知道了這個(gè)之后,我們就可以回答文章最開始那個(gè)問(wèn)題了,在隔離級(jí)別為可重復(fù)讀的情況下(這里的隱含條件就是可重復(fù)讀隔離級(jí)別只會(huì)在第一次查詢時(shí)生成一個(gè)視圖,之后在這個(gè)事務(wù)中的所有查詢操作都會(huì)重復(fù)使用這個(gè)視圖)分析一波:
以文章開頭的例子,設(shè)定事務(wù)B的事務(wù)id=100,事務(wù)C的事務(wù)id=200,當(dāng)事務(wù)B尚未提交時(shí),id=1這條記錄的版本鏈?zhǔn)沁@樣的:
這個(gè)時(shí)候我們看一下事務(wù)A第一個(gè)select語(yǔ)句,注意查詢操作的事務(wù)trx_id=0,在執(zhí)行select語(yǔ)句時(shí)會(huì)創(chuàng)建一個(gè)視圖,這個(gè)視圖的m_ids={100},min_trx_id=100,max_trx_id=101,creator_trx_id=0。
然后在版本鏈中挑選可見的數(shù)據(jù)記錄,從圖中可以看到最新版本的name值是B,最新版本的trx_id值為100,在m_ids集合中,這個(gè)版本數(shù)據(jù)不可見,根據(jù)roll_point跳到下一個(gè)版本;
下一個(gè)版本的name值是A,這個(gè)版本的trx_id=99,小于min_trx_id,這個(gè)版本數(shù)據(jù)是可見的,所以返回name為A的記錄,即V1為A。
我們繼續(xù),事務(wù)B這時(shí)進(jìn)行了commit提交,此時(shí)事務(wù)C已經(jīng)開啟,那么事務(wù)A第二個(gè)select語(yǔ)句不會(huì)創(chuàng)建一個(gè)新的視圖,而是重新利用第一次創(chuàng)建的視圖。最新版本的trx_id為100,在m_ids中,數(shù)據(jù)不可見,即V2=A;
接下來(lái),事務(wù)C進(jìn)行了更新操作,此時(shí)版本鏈發(fā)生的改變?nèi)缦拢?/p>
事務(wù)C接著進(jìn)行了commit提交,此時(shí)事務(wù)A第三次select語(yǔ)句也不會(huì)創(chuàng)建一個(gè)新的視圖,最新版本的trx_id為200,大于max_trx_id,數(shù)據(jù)不可見,即V3=A。
到這里,MVCC就結(jié)束啦,留一個(gè)小問(wèn)題,如果是讀已提交隔離級(jí)別,那么文章開頭的例子中V1、V2、V3的值又分別是什么呢?答案在最后哦。
最后,我們?cè)賮?lái)總結(jié)一下MVCC的作用,使用可重復(fù)讀隔離級(jí)別的事務(wù)在查詢時(shí),僅會(huì)使用第一次select時(shí)生成的視圖,相比于讀已提交隔離級(jí)別每次查詢都會(huì)生成一個(gè)新的視圖,可重復(fù)讀在查詢時(shí)使用的視圖版本不會(huì)那么新,因此有些已經(jīng)提交的事務(wù)對(duì)行記錄進(jìn)行修改時(shí)對(duì)查詢事務(wù)就不可見,進(jìn)而避免了不可重復(fù)讀現(xiàn)象的發(fā)生,同時(shí)也避免了臟讀。
小問(wèn)題答案:
讀已提交隔離級(jí)別下,每次select查詢都會(huì)生成一個(gè)新的視圖,基于此,分析如下:
事務(wù)A第一個(gè)select語(yǔ)句,注意查詢操作的事務(wù)trx_id=0,在執(zhí)行select語(yǔ)句時(shí)會(huì)創(chuàng)建一個(gè)視圖,這個(gè)視圖的m_ids={100},min_trx_id=100,max_trx_id=101,creator_trx_id=0。
然后在版本鏈中挑選可見的數(shù)據(jù)記錄,從圖中可以看到最新版本的name值時(shí)B,最新版本的trx_id值為100,在m_ids集合中,這個(gè)版本數(shù)據(jù)不可見,根據(jù)roll_point跳到下一個(gè)版本;
下一個(gè)版本的name值是A,這個(gè)版本的trx_id=99,小于min_trx_id,這個(gè)版本數(shù)據(jù)是可見的,所以返回name為A的記錄,即V1為A。
事務(wù)B這時(shí)進(jìn)行了commit提交,此時(shí)事務(wù)C已經(jīng)開啟,那么事務(wù)A第二個(gè)select語(yǔ)句會(huì)創(chuàng)建一個(gè)新的視圖,這個(gè)視圖的m_ids={200},min_trx_id=200,max_trx_id=201,creator_trx_id=0。版本鏈沒(méi)有發(fā)生變化,最新版本trx_id值為100,小于min_trx_id,數(shù)據(jù)可見,即V2=B;文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-410280.html
事務(wù)C接著進(jìn)行了commit提交,此時(shí)事務(wù)A第三次select語(yǔ)句會(huì)創(chuàng)建一個(gè)新的視圖,這個(gè)視圖的m_ids={},min_trx_id不存在,max_trx_id=201,creator_trx_id=0。在版本鏈中挑選可見的數(shù)據(jù)記錄,從圖中可以看到最新版本的name值為C,最新版本的trx_id值為200,小于max_trx_id且不在m_ids中,則數(shù)據(jù)可見,即V3=C。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-410280.html
到了這里,關(guān)于一文了解MySQL中的多版本并發(fā)控制的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!