一文精通MVCC機(jī)制
MVCC(Multi-Version Concurrency Control)多版本并發(fā)控制機(jī)制
使用串行化隔離級(jí)別時(shí),mysql會(huì)將所有的操作加鎖互斥,來(lái)保證并發(fā)安全。這種方式必然降低并發(fā)性能。mysql在讀已提交和可重復(fù)讀隔離級(jí)別下,對(duì)一行數(shù)據(jù)的讀和寫兩個(gè)操作默認(rèn)是不會(huì)通過(guò)加鎖互斥來(lái)保證隔離性,避免了頻繁加鎖互斥。那么具體是如何實(shí)現(xiàn)的呢?首先要了解兩個(gè)概念。
準(zhǔn)備
建表語(yǔ)句
CREATE TABLE `product` ( ?`id` int NOT NULL AUTO_INCREMENT, ?`name` varchar(255) DEFAULT NULL, ?`price` int DEFAULT NULL, ?PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
undo日志版本鏈
我們向product表插入一條數(shù)據(jù)
INSERT INTO mysql_demo.product (id, name, price) VALUES (1, 'apple', 10);

此時(shí)mysql會(huì)同時(shí)向undo日志里寫入一條記錄。 trx_id為插入操作的事務(wù)id。這里隨便寫了一個(gè)80,意思一下。 roll_pointer后面再說(shuō)。

這時(shí)候又來(lái)了一個(gè)事務(wù),對(duì)數(shù)據(jù)進(jìn)行了修改。比如事務(wù)id 300,修改price為20。此時(shí)mysql同樣會(huì)在undo日志里寫入一條記錄。并且roll_pointer會(huì)指向前一條記錄

以此類推,后續(xù)又有新的事務(wù)來(lái)操作這條記錄,就會(huì)形成一條版本鏈,這條鏈就是undo日志版本鏈。

每條數(shù)據(jù)對(duì)應(yīng)著有一個(gè)undo日志版本鏈。
對(duì)于insert和update操作,mysql會(huì)向undo日志里添加一條記錄。select操作不會(huì)產(chǎn)生記錄。
對(duì)于刪除的情況可以認(rèn)為是update的特殊情況,會(huì)將版本鏈上最新的數(shù)據(jù)復(fù)制一份,然后將trx_id修改成刪除操作的trx_id,同時(shí)在該條記錄的頭信息(record header)里的(deleted_flag)標(biāo)記位寫上true,來(lái)表示當(dāng)前記錄已經(jīng)被刪除,在查詢時(shí)按照上面的規(guī)則查到對(duì)應(yīng)的記錄如果delete_flag標(biāo)記位為true,意味著記錄已被刪除,則不返回?cái)?shù)據(jù)。
在來(lái)看下什么是read view。
一致性試圖read view機(jī)制
read view的生成
可重復(fù)讀隔離級(jí)別:事務(wù)開(kāi)啟后,首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view,在事務(wù)結(jié)束前不會(huì)變化。
讀已提交隔離級(jí)別:事務(wù)開(kāi)啟后,每次執(zhí)行select時(shí)都會(huì)重新生成read-view。
read view的組成
這個(gè)視圖由執(zhí)行查詢時(shí)所有未提交事務(wù)id數(shù)組(數(shù)組里最小的id為min_id)和已創(chuàng)建的最大事務(wù)id(max_id)組成。

我們來(lái)舉個(gè)例子。
Transaction 80: 開(kāi)啟事務(wù),插入一條記錄。并且commit;
Transaction 100:開(kāi)啟事務(wù),執(zhí)行update。生成事務(wù)id 100。這里需要注意begin和select不會(huì)生成事務(wù)id,所以加了一條無(wú)關(guān)的update,生成事務(wù)id。update內(nèi)容可以忽略。
begin/start transaction 命令并不是一個(gè)事務(wù)的起點(diǎn),在執(zhí)行到它們之后的第一個(gè)修改操作InnoDB表的語(yǔ)句,事務(wù)才真正啟動(dòng),才會(huì)向mysql申請(qǐng)事務(wù)id
mysql內(nèi)部是嚴(yán)格按照事務(wù)的啟動(dòng)順序來(lái)分配事務(wù)id的
Transaction 200:同上
Transaction 300:把價(jià)格修改成20了。并且commit了。
select 1: select 不生成事務(wù)id。 事務(wù)開(kāi)啟后,首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view。
Transaction 400:把價(jià)格修改成18了。
read view的組成 = 未提交事務(wù)id數(shù)組(數(shù)組里最小的id為min_id) + 已創(chuàng)建的最大事務(wù)id(max_id)組成
此時(shí)未提交事務(wù)id有100,200(80 已經(jīng)提交了)。最小的id為100。 已創(chuàng)建的最大事務(wù)id為300。(注意read view 是在第5步生成的,此時(shí)還沒(méi)有Transaction 400)
因此 read view為[100,200],300 ? min_id為100 ,max_id為300。 ?[100,200] 為視圖數(shù)組。
此時(shí)對(duì)應(yīng)的undo日志版本鏈如下

那么read view 的作用是什么呢?
read view的作用
根據(jù)上面的結(jié)果,我們可以將事務(wù)進(jìn)行分類。因?yàn)槭聞?wù)的id是有序遞增的。所以我們可以得出以下結(jié)論
因?yàn)槲刺峤皇聞?wù)的最小id(min_id)為100,所以小于100的事務(wù)都是已提交的。( Transaction 80)
因?yàn)橐褎?chuàng)建的最大事務(wù)id(max_id)為300,所以大于300的區(qū)域都是未開(kāi)啟事務(wù)。 (Transaction 400) ?未開(kāi)啟理解為在執(zhí)行select的時(shí)候沒(méi)有開(kāi)啟。
介于min_id和max_id之間的事務(wù),包含了未提交和已提交的事務(wù)。 (Transaction 100,200,300)

那么mysql是如何通過(guò)read view和undo日志版本鏈實(shí)現(xiàn)并發(fā)事務(wù)之間的隔離的呢?那就需要看下版本鏈比對(duì)規(guī)則了。
版本鏈比對(duì)規(guī)則
事務(wù)里的每一條select都需要從對(duì)應(yīng)版本鏈里的最新數(shù)據(jù)開(kāi)始逐條跟read-view做比對(duì),按照比對(duì)規(guī)則得到最終的快照結(jié)果。下面我們來(lái)看下版本鏈比對(duì)規(guī)則。
如果 row 的 trx_id 落在綠色部分( trx_id
如果 row 的 trx_id 落在灰色部分( trx_id>max_id ),表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的
row 的 trx_id 就是當(dāng)前自己的事務(wù)是可見(jiàn)的;
否則不可見(jiàn);
如果 row 的 trx_id 落在黃色部分(min_id <=trx_id<= max_id),那就包括兩種情況
若 row 的 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,
若 row 的 trx_id 就是當(dāng)前自己的事務(wù)是可見(jiàn)的
否則不可見(jiàn);
若 row 的 trx_id 不在視圖數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,可見(jiàn)。
知道了版本鏈的比對(duì)規(guī)則,下面我們通過(guò)實(shí)例來(lái)看下,mysql的MVCC機(jī)制是如何工作的。
實(shí)戰(zhàn)演練
可重復(fù)讀Repeatable-Read(RR)
我們先以可重復(fù)讀Repeatable-Read(RR)為例
可重復(fù)讀隔離級(jí)別:事務(wù)開(kāi)啟后,首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view,在事務(wù)結(jié)束前不會(huì)變化。
案例一
我們先以上面的情況為例來(lái)進(jìn)行分析。此時(shí)的情況如下:

read view為 [100,200],300

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 300,trx_id = max_id。此時(shí)繼續(xù)比對(duì), trx_id 不在視圖數(shù)組中,可見(jiàn)。
返回Transaction 300記錄的數(shù)據(jù)信息。price = 20;
案例二

Transaction 400,在第10行執(zhí)行了一次update。
Transaction 100,在第11,12行執(zhí)行了兩次update。然后select 1 13行執(zhí)行了一次select。 我們來(lái)分析下這個(gè)select。
因?yàn)镽R隔離級(jí)別首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view,在事務(wù)結(jié)束前不會(huì)變化。所以read view為 [100,200],300。沒(méi)有變化。

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 100,trx_id = min_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
第二行Transaction 100,分析同上
第三行Transaction 400, trx_id > max_id,不可見(jiàn)。
Transaction 300,trx_id = max_id。此時(shí)繼續(xù)比對(duì),trx_id 不在視圖數(shù)組中,可見(jiàn)。
返回Transaction 300記錄的數(shù)據(jù)信息。price = 20;
案例三

繼續(xù)向下Transaction 100,在第15行commit。Transaction 200,在第15,16行執(zhí)行了兩次update。然后select1 17行執(zhí)行了一次select。 我們來(lái)分析下這個(gè)select。
因?yàn)镽R隔離級(jí)別首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view,在事務(wù)結(jié)束前不會(huì)變化。所以read view為 [100,200],300。沒(méi)有變化。

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 200,min_id < trx_id < max_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
下一行Transaction 200,分析同上.
Transaction 100,trx_id = min_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
下一行Transaction 100,分析同上。
下一行Transaction 400, trx_id > max_id,不可見(jiàn)。
Transaction 300,trx_id = max_id。此時(shí)繼續(xù)比對(duì),trx_id 不在視圖數(shù)組中,可見(jiàn)。
返回Transaction 300記錄的數(shù)據(jù)信息。price = 20;
案例四

繼續(xù)select2 17行執(zhí)行了一次select。 我們來(lái)分析下這個(gè)select。
RR隔離級(jí)別首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view。read view為 [200,400],400。

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 200,trx_id = min_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
下一行Transaction 200,分析同上.
Transaction 100,trx_id < min_id。表示這個(gè)版本是已提交的事務(wù)生成的,這個(gè)數(shù)據(jù)是可見(jiàn)的;
返回 price = 16。
案例五
我們?cè)賮?lái)看一下如果select1 如果有update操作(update操作會(huì)創(chuàng)建事務(wù)id,我們假設(shè)是 500)。Transaction 500 此時(shí)是如何讀取到更新后的數(shù)據(jù)的。

來(lái)分析下15行。
RR隔離級(jí)別首次執(zhí)行任何select時(shí)會(huì)生成當(dāng)前事務(wù)的read-view,在事務(wù)結(jié)束前不會(huì)變化。read view為 [100,200],300。
undo日志版本鏈如下
套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 400,trx_id > max_id(read view是第一次select時(shí)生成的,此時(shí)max_id仍然是 300)。表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,是不可見(jiàn)的
Transaction 500,trx_id > max_id。表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,但row 的 trx_id 就是當(dāng)前自己的事務(wù)是可見(jiàn)的;所以可見(jiàn)
返回 price = 8。
結(jié)論:通過(guò)以上案例,我們可以知道。 MVCC機(jī)制在RR中首次查詢時(shí)會(huì)固定read view。后續(xù)和其他事務(wù)隔離開(kāi)了,其他事務(wù)對(duì)數(shù)據(jù)的操作不會(huì)影響到當(dāng)前事務(wù)。
讀已提交Read-Committed(RC)
我們?cè)僖宰x已提交Read-Committed(RC)為例
讀已提交隔離級(jí)別:事務(wù)開(kāi)啟后,每次執(zhí)行select時(shí)都會(huì)重新生成read-view。
案例一

第9行沒(méi)有變化,我們來(lái)看第13行。
read view的組成 = 未提交事務(wù)id數(shù)組(數(shù)組里最小的id為min_id) + 已創(chuàng)建的最大事務(wù)id(max_id)組成
未提交事務(wù)id數(shù)組 100,200,400 ; ? ?min_id 100 ; max_id 400
read view為 [100,200,400],400。

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 100,trx_id = min_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
下一行Transaction 100,分析同上.
Transaction 400, trx_id = max_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
Transaction 300,min_id < trx_id< max_id。不在視圖數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,可見(jiàn)。
返回 price = 20。
案例二

來(lái)看第17行。
read view的組成 = 未提交事務(wù)id數(shù)組(數(shù)組里最小的id為min_id) + 已創(chuàng)建的最大事務(wù)id(max_id)組成
未提交事務(wù)id數(shù)組 200,400 ; ? ?min_id 200 ; max_id 400
read view為 [200,400],400。

undo日志版本鏈如下

套用版本鏈比對(duì)規(guī)則
首先在版本鏈中找到最新數(shù)據(jù)。
Transaction 200, trx_id = min_id。繼續(xù)分析 trx_id 在視圖數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn)。
同上
Transaction 100, trx_id
返回 price = 16。
OK,就分析到這里吧。希望對(duì)你有所幫助!