首先看一個(gè)例子:
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
往test表插入兩條數(shù)據(jù):
INSERT INTO `test` (`id`, `c`) VALUES (1, 1);
INSERT INTO `test` (`id`, `c`) VALUES (2, 2);
然后我們對(duì)這張表做如下操作:
事務(wù)的啟動(dòng)機(jī)制:begin/start transaction 命令并不是一個(gè)事務(wù)的起點(diǎn)肛走,在執(zhí)行到它們之后的第一個(gè)操作InnoDB 表的語(yǔ)句恶复,事務(wù)才真正啟動(dòng)详羡。如果我們想馬上啟動(dòng)一個(gè)事務(wù)州弟,可以用 start transaction with consistent snapshot 命令钧栖。
第一種啟動(dòng)方式,一致性視圖是在執(zhí)行第一個(gè)快照讀語(yǔ)句時(shí)創(chuàng)建的婆翔。
第二種啟動(dòng)方式拯杠,一致性視圖是在執(zhí)行 start transaction with consistent snapshot 時(shí)創(chuàng)建的。
在MySQL中有兩個(gè)“視圖”的概念:
- 一個(gè)是view啃奴。它是一個(gè)用查詢(xún)語(yǔ)句定義的虛擬表潭陪,在調(diào)用的時(shí)候執(zhí)行查詢(xún)語(yǔ)句并生成結(jié)果。創(chuàng)建視圖的語(yǔ)法是create view...最蕾,而它的查詢(xún)方法和查詢(xún)表一樣依溯。
- 另一個(gè)是InnoDB在實(shí)現(xiàn)MVCC時(shí)用到的一致性視圖,即consistent read view瘟则,用于支持讀提交(Read Committed)和可重復(fù)讀(Repeatable Read)隔離級(jí)別的實(shí)現(xiàn)黎炉。
InnoDB是怎么秒級(jí)創(chuàng)建快照的
在可重復(fù)讀隔離級(jí)別下,事務(wù)啟動(dòng)時(shí)就會(huì)基于整個(gè)數(shù)據(jù)庫(kù)創(chuàng)建一個(gè)快照醋拧。
快照的實(shí)現(xiàn):
快照并不是把數(shù)據(jù)庫(kù)所有的數(shù)據(jù)都拷貝一邊存放慷嗜。
在InnoDB里淀弹,每個(gè)事務(wù)都有一個(gè)唯一的transaction id。它是在事務(wù)開(kāi)始的時(shí)候向InnoDB的事務(wù)系統(tǒng)申請(qǐng)的洪添,是按準(zhǔn)許嚴(yán)格遞增的垦页。
每行數(shù)據(jù)都會(huì)有多個(gè)版本。每次事務(wù)更新數(shù)據(jù)的時(shí)候干奢,都會(huì)生成一個(gè)新的數(shù)據(jù)版本痊焊,并把transaction id 賦值給這個(gè)數(shù)據(jù)版本的事務(wù)ID,叫做row trx_id忿峻。同時(shí)薄啥,舊的數(shù)據(jù)版本也要保留,并在新的數(shù)據(jù)版本中可以找到舊的數(shù)據(jù)版本逛尚。
簡(jiǎn)單的說(shuō)就是垄惧,數(shù)據(jù)表中的一行記錄可能有多個(gè)版本(row),每個(gè)版本有自己的row trx_id绰寞。
如圖所示:
圖中同一行數(shù)據(jù)共有四個(gè)版本到逊,當(dāng)前版本是V4,c的值是8滤钱,它是被transaction id = 20的事務(wù)更新的觉壶,所以它的row trx_id也是20。
圖中的三個(gè)紅色箭頭就是undo log件缸;其實(shí)V1铜靶,V2,V3他炊,并不是物理上真實(shí)存在的争剿,而是每次更新的時(shí)候根據(jù)當(dāng)前版本和undo log 計(jì)算出來(lái)的。比如痊末,我們想得到V2版本時(shí)候的值蚕苇,就需要從V4依次執(zhí)行U3,U2得到舌胶。
可重復(fù)讀定義:一個(gè)事務(wù)開(kāi)啟的時(shí)候捆蜀,能夠看到這個(gè)時(shí)候開(kāi)啟那一刻所有已經(jīng)提交的事務(wù)結(jié)果。但開(kāi)啟之后幔嫂,這個(gè)事務(wù)commit之前,其他新的事務(wù)的更新對(duì)它是不可見(jiàn)的誊薄。
實(shí)現(xiàn)方式:
InnoDB為每個(gè)事務(wù)構(gòu)造了一個(gè)數(shù)組履恩,用來(lái)保存這個(gè)事務(wù)啟動(dòng)瞬間,當(dāng)前已經(jīng)啟動(dòng)但還未提交的事務(wù)ID呢蔫。
數(shù)組里面最小的事務(wù)ID標(biāo)記為低水位切心,當(dāng)前系統(tǒng)中已經(jīng)創(chuàng)建過(guò)的事務(wù)ID的最大值加1標(biāo)記為高水位飒筑。
注意:低水位是針對(duì)事務(wù)數(shù)組里最小的事務(wù)ID,針對(duì)的是事務(wù)數(shù)組绽昏;高水位是當(dāng)前系統(tǒng)創(chuàng)建過(guò)的事務(wù)ID最大值加1协屡,針對(duì)的是當(dāng)前系統(tǒng);
這個(gè)視圖數(shù)組和高水位組成了當(dāng)前事務(wù)的一致性視圖(read-view)全谤。
數(shù)據(jù)的可見(jiàn)性規(guī)則就是基于數(shù)據(jù)的row trx_id和一致性視圖的對(duì)比結(jié)果得到的肤晓。
從圖中可以看出,當(dāng)前事務(wù)啟動(dòng)瞬間认然,一個(gè)數(shù)據(jù)版本的row trx_id有以下幾種可能:
如果落在綠色部分补憾,表示這個(gè)版本是已經(jīng)提交的事務(wù)或者是當(dāng)前事務(wù)自己生成的,可見(jiàn)卷员。
如果落在紅色部分盈匾,表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,不可見(jiàn)毕骡。
-
如果落在黃色部分削饵,包含兩種情況:
a. 如果row trx_id 在數(shù)組中,表示這個(gè)版本是還沒(méi)提交的事務(wù)生成的未巫,不可見(jiàn)窿撬。
b. 如果row trx_id 不在數(shù)組中,表示這個(gè)版本是已經(jīng)提交的事務(wù)生成的橱赠,可見(jiàn)尤仍。
接下來(lái)我們分析一下開(kāi)篇的那個(gè)例子,分析事務(wù)A返回的結(jié)果狭姨。
我們可以假設(shè)一下:
- 事務(wù)A開(kāi)始前宰啦,系統(tǒng)里面只有一個(gè)活躍事務(wù)(啟動(dòng)但為提交)ID=99;
- 事務(wù)A饼拍,B赡模,C的版本號(hào)分別為100,101师抄,102漓柑,而且當(dāng)前系統(tǒng)中只有這四個(gè)事務(wù);
- 三個(gè)事務(wù)開(kāi)始前叨吮,c=1這行數(shù)據(jù)的row trx_id是90辆布。
這樣,事務(wù) A 的視圖數(shù)組就是[99,100], 事務(wù) B 的視圖數(shù)組是[99,100,101], 事務(wù) C 的視圖數(shù)組是[99,100,101,102]茶鉴。
從圖中可以看出锋玲,事務(wù)C把c=1改成了c=2,這時(shí)候數(shù)據(jù)版本的row trx_id = 102涵叮。
事務(wù)B把c=2改成了c=3惭蹂,這時(shí)候數(shù)據(jù)版本的row trx_id = 101伞插。
可重復(fù)讀隔離級(jí)別下,事務(wù)A查詢(xún)的時(shí)候盾碗,事務(wù)B還沒(méi)有提交媚污,所以c=3對(duì)事務(wù)A來(lái)說(shuō)是不可見(jiàn)的,否則就是臟讀了廷雅。
現(xiàn)在事務(wù)A的視圖數(shù)組是[99,100]耗美,讀數(shù)據(jù)都是從當(dāng)前版本開(kāi)始讀的,所以事務(wù)A查詢(xún)語(yǔ)句讀數(shù)據(jù)流程如下:
- 找到c=3的時(shí)候榜轿,判斷row trx_id=101幽歼,比高水位大,處于紅色區(qū)域谬盐,不可見(jiàn)甸私;
- 繼續(xù)找歷史版本,找到 row trx_id=102飞傀,比高水位大皇型,處于紅色區(qū)域,不可見(jiàn)砸烦;
- 繼續(xù)找歷史版本弃鸦,找到row trx_id=90,比低水位小幢痘,處于綠色區(qū)域唬格,可見(jiàn)。
所以事務(wù)A查詢(xún)得到的結(jié)果c=1颜说。
所以一個(gè)數(shù)據(jù)版本购岗,對(duì)于一個(gè)事務(wù)視圖來(lái)說(shuō),除了自己更新的總是可見(jiàn)以外门粪,有三種情況:
- 版本未提交喊积,可見(jiàn);
- 版本已提交玄妈,但是是在視圖創(chuàng)建后提交乾吻,不可見(jiàn);
- 版本已提交拟蜻,但是是在視圖創(chuàng)建前提交绎签,可見(jiàn)。
在這個(gè)例子中酝锅,事務(wù)B是在事務(wù)C之前啟動(dòng)的辜御,那為什么事務(wù)B算出來(lái)的c=3呢?
因?yàn)槭聞?wù)B在更新之前屈张,是要先讀一次當(dāng)前版本數(shù)據(jù)的擒权,然后再在當(dāng)前版本的數(shù)據(jù)基礎(chǔ)之上做的更新,要不然事務(wù)C的更新就丟了阁谆。
所以這里有一條規(guī)則:更新數(shù)據(jù)都是先讀后寫(xiě)碳抄,這個(gè)讀,只能讀當(dāng)前版本的數(shù)據(jù)场绿,稱(chēng)為“當(dāng)前讀”(current read)剖效。
所以,當(dāng)事務(wù)B更新之前焰盗,當(dāng)前都得到的c=2璧尸,更新后生成了新的版本數(shù)據(jù)c=3,新版本的row trx_id=101熬拒,然后事務(wù)B查詢(xún)時(shí)拿到的row trx_id=101和自己的版本相同爷光,是自己更新的,可以直接使用澎粟,所以事務(wù)B查詢(xún)得到的c=3蛀序。
除了update語(yǔ)句是當(dāng)前讀之外,如果select語(yǔ)句加鎖活烙,也是當(dāng)前讀徐裸。
select c from test where id=1 lock in share mode;
select c from test where id=1 for update;
上面兩條語(yǔ)句中,第一條語(yǔ)句加讀鎖(S鎖啸盏,共享鎖)重贺,第二條語(yǔ)句加寫(xiě)鎖(X鎖,排他鎖)回懦。
如果我們改一下例子中的事務(wù)A語(yǔ)句气笙,加上lock in share mode或者for update,返回結(jié)果c=3.
讀提交的邏輯和可重復(fù)讀的邏輯是類(lèi)似的粉怕,主要的區(qū)別就是:
- 在可重復(fù)讀隔離級(jí)別下健民,只需要在事務(wù)開(kāi)始的時(shí)候創(chuàng)建一致性視圖,之后事務(wù)里的其他查詢(xún)都共用這個(gè)一致性視圖贫贝;
- 在讀提交隔離級(jí)別下秉犹,每一個(gè)語(yǔ)句執(zhí)行前都會(huì)重新算出一個(gè)新的視圖。
結(jié)束稚晚!