日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
MySQLMVCC實(shí)現(xiàn)原理

1、概念

MVCC (Multiversion Concurrency Control),多版本并發(fā)控制。顧名思義,MVCC是通過(guò)數(shù)據(jù)行的多個(gè)版本管理實(shí)現(xiàn)數(shù)據(jù)庫(kù)的并發(fā)控制。這項(xiàng)技術(shù)使得在InnoDB的事務(wù)隔離級(jí)別下執(zhí)行一致性讀操作有了保證。換言之,就是為了查詢一些正在被另一個(gè)事務(wù)更新的行,并且可以看到它們被更新之前的值,這樣在做查詢的時(shí)候就不用等待另一個(gè)事務(wù)釋放鎖。

MVCC沒(méi)有正式的標(biāo)準(zhǔn),在不同的DBMS中MVCC的實(shí)現(xiàn)方式可能是不同的,也不是普遍使用的。本文講解InnoDB中MVCC的實(shí)現(xiàn)機(jī)制(MySQL其它的存儲(chǔ)引擎并不支持它)。

2、快照讀和當(dāng)前讀

MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫(kù)并發(fā)性能,用更好的方式去處理讀-寫(xiě)沖突,做到即使有讀寫(xiě)沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀,而這個(gè)讀指的就是快照讀,而非當(dāng)前讀。當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)。而MVCC本質(zhì)是采用樂(lè)觀鎖思想的一種方式。

2.1 快照讀

快照讀又叫一致性讀,讀取的是快照數(shù)據(jù)。不加鎖的簡(jiǎn)單的SELECT都屬于快照讀,即不加鎖的非阻塞讀。比如這樣:

SELECT * FROM player WHERE ...

之所以出現(xiàn)快照讀的情況,是基于提高并發(fā)性能的考慮,快照讀的實(shí)現(xiàn)是基于MVCC,它在很多情況下,避免了加鎖操作,降低了開(kāi)銷(xiāo)。既然是基于多版本,那么快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。快照讀的前提是隔離級(jí)別不是串行級(jí)別,串行級(jí)別下的快照讀會(huì)退化成當(dāng)前讀。

2.2 當(dāng)前讀

當(dāng)前讀讀取的是記錄的最新版本(最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)),讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖。加鎖的SELECT,或者對(duì)數(shù)據(jù)進(jìn)行增刪改都會(huì)進(jìn)行當(dāng)前讀。比如:

SELECT * FROM student LOCK IN SHARE MODE; #共享鎖
SELECT * FROM student FOR UPDATE; #排他鎖
INSERT INTO student values ... #排他鎖
DELETE FROM student WHERE ... #排他鎖
UPDATE student SET ... #排他鎖

3、MVCC實(shí)現(xiàn)

3.1 隱藏字段

對(duì)于使用InnoDB存儲(chǔ)引擎的表來(lái)說(shuō),它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列。

trx_id:每次一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù)id賦值給trx_id隱藏列。

roll_pointer:每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫(xiě)入到undo日志中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過(guò)它來(lái)找到該記錄修改前的信息。

3.2 Undo Log版本鏈

舉例: student表數(shù)據(jù)如下

SELECT * FROM student ;
/*
+----+--------+--------+
| id | name | class |
+----+--------+--------+
| 1 | 張三 | 一班 |
+----+--------+--------+
1 row in set (0.07 sec)
*/

假設(shè)插入該記錄的事務(wù)id為8,那么此刻該條記錄的示意圖如下所示:

insert undo只在事務(wù)回滾時(shí)起作用,當(dāng)事務(wù)提交后,該類(lèi)型的undo日志就沒(méi)用了,它占用的Undo Log Segment也會(huì)被系統(tǒng)回收(也就是該undo日志占用的Undo頁(yè)面鏈表要么被重用,要么被釋放)。

假設(shè)之后兩個(gè)事務(wù)id分別為10、20的事務(wù)對(duì)這條記錄進(jìn)行UPDATE 操作,操作流程如下:

發(fā)生時(shí)間順序

事務(wù)10

事務(wù)20

1

BEGIN;

2

BEGIN;

3

UPDATE student SET name=“李四” WHERE id=1;

4

UPDATE student SET name=“王五” WHERE id=1;

5

COMMIT;

6

UPDATE student SET name=“錢(qián)七” WHERE id=1;

7

UPDATE student SET name=“宋八” WHERE id=1;

8

COMMIT;

能不能在兩個(gè)事務(wù)中交叉更新同一條記錄呢?

不能!這就是一個(gè)事務(wù)修改了另一個(gè)未提交事務(wù)修改過(guò)的數(shù)據(jù),臟寫(xiě)。

InnoDB使用鎖來(lái)保證不會(huì)有臟寫(xiě)情況的發(fā)生,也就是在第一個(gè)事務(wù)更新了某條記錄后,就會(huì)給這條記錄加鎖,另一個(gè)事務(wù)再次更新時(shí)就需要等待第一個(gè)事務(wù)提交了,把鎖釋放之后才可以繼續(xù)更新。

每次對(duì)記錄進(jìn)行改動(dòng),都會(huì)記錄一條undo日志,每條undo日志也都有一個(gè)roll_pointer屬性(INSERT操作對(duì)應(yīng)的undo日志沒(méi)有該屬性,因?yàn)樵撚涗洸](méi)有更早的版本),可以將這些undo日志都連起來(lái),串成一個(gè)鏈表:

對(duì)該記錄每次更新后,都會(huì)將舊值放到一條undo日志中,就算是該記錄的一個(gè)舊版本,隨著更新次數(shù)的增多,所有的版本都會(huì)被roll_pointer屬性連接成一個(gè)鏈表,把這個(gè)鏈表稱(chēng)之為版本鏈,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù)id。

3.3 ReadView

在MVCC機(jī)制中,多個(gè)事務(wù)對(duì)同一個(gè)行記錄進(jìn)行更新會(huì)產(chǎn)生多個(gè)歷史快照,這些歷史快照保存在Undo Log里。如果一個(gè)事務(wù)想要查詢這個(gè)行記錄,需要讀取哪個(gè)版本的行記錄呢?這時(shí)就需要用到ReadView了,它幫我們解決了行的可見(jiàn)性問(wèn)題。

ReadView就是事務(wù)在使用MVCC機(jī)制進(jìn)行快照讀操作時(shí)產(chǎn)生的讀視圖。當(dāng)事務(wù)啟動(dòng)時(shí),會(huì)生成數(shù)據(jù)庫(kù)系統(tǒng)當(dāng)前的一個(gè)快照,InnoDB為每個(gè)事務(wù)構(gòu)造了一個(gè)數(shù)組,用來(lái)記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的ID(“活躍”指的就是,啟動(dòng)了但還沒(méi)提交)。

3.3.1 設(shè)計(jì)思路

使用READ UNCONNMITTED隔離級(jí)別的事務(wù),由于可以讀到未提交事務(wù)修改過(guò)的記錄,所以直接讀取記錄的最新版本就好了。

使用SERIALIZABLE隔離級(jí)別的事務(wù),InnoDB規(guī)定使用加鎖的方式來(lái)訪問(wèn)記錄。

使用 READ COMMITTED 和 REPEATABLE READ 隔離級(jí)別的事務(wù),都必須保證讀到 已經(jīng)提交了的 事務(wù)修改過(guò)的記錄。假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問(wèn)題就是需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見(jiàn)的,這是ReadView要解決的主要問(wèn)題。

這個(gè)ReadView中主要包含4個(gè)比較重要的內(nèi)容,分別如下:

creator_trx_id ,創(chuàng)建這個(gè) Read View 的事務(wù) ID。

說(shuō)明:只有在對(duì)表中的記錄做改動(dòng)時(shí)(執(zhí)行INSERT、DELETE、UPDATE這些語(yǔ)句時(shí))才會(huì)為事務(wù)分配事務(wù)id,否則在一個(gè)只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。

trx_ids ,表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫(xiě)事務(wù)的事務(wù)id列表 。

up_limit_id ,活躍的事務(wù)中最小的事務(wù) ID。

low_limit_id ,表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值。low_limit_id 是系統(tǒng)最大的事務(wù)id值,這里要注意是系統(tǒng)中的事務(wù)id,需要區(qū)別于正在活躍的事務(wù)ID。

注意:low_limit_id并不是trx_ids中的最大值,事務(wù)id是遞增分配的。比如,現(xiàn)在有id為1,2,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成ReadView時(shí),trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

舉例:

trx_ids為trx2、trx3、trx5和trx8的集合,系統(tǒng)的最大事務(wù)ID (low_limit_id)為trx8+1(如果之前沒(méi)有其他的新增事務(wù)),活躍的最小事務(wù)ID (up_limit_id)為trx2。

3.3.2 ReadView的規(guī)則

有了這個(gè)ReadView,這樣在訪問(wèn)某條記錄時(shí),只需要按照下邊的步驟判斷記錄的某個(gè)版本是否可見(jiàn)。

如果被訪問(wèn)版本的trx_id屬性值與ReadView中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。20可以訪問(wèn)自己

如果被訪問(wèn)版本的trx_id屬性值小于ReadView中的 up_limit_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。

如果被訪問(wèn)版本的trx_id屬性值大于或等于ReadView中的 low_limit_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開(kāi)啟,所以該版本不可以被當(dāng)前事務(wù)訪問(wèn)。

如果被訪問(wèn)版本的trx_id屬性值在ReadView的 up_limit_id 和 low_limit_id之間,那就需要判斷一下trx_id屬性值是不是在 trx_ids 列表中。

如果在,說(shuō)明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問(wèn)。

如果不在,說(shuō)明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問(wèn)。

3.4 MVCC整體操作流程

了解了這些概念之后,來(lái)看下當(dāng)查詢一條記錄的時(shí)候,系統(tǒng)如何通過(guò)MVCC找到它:

首先獲取事務(wù)自己的版本號(hào),也就是事務(wù) ID;

獲取 ReadView;

查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號(hào)進(jìn)行比較;

如果不符合 ReadView 規(guī)則,就需要從 Undo Log 中獲取歷史快照;

最后返回符合規(guī)則的數(shù)據(jù)。

如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見(jiàn)的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見(jiàn)性,依此類(lèi)推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見(jiàn)的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見(jiàn),查詢結(jié)果就不包含該記錄。InnoDB中,MVCC是通過(guò)Undo Log + Read View進(jìn)行數(shù)據(jù)讀取,Undo Log保存了歷史快照,而Read View規(guī)則幫我們判斷當(dāng)前版本的數(shù)據(jù)是否可見(jiàn)。在隔離級(jí)別為讀已提交(Read Committed)時(shí),一個(gè)事務(wù)中的每一次 SELECT 查詢都會(huì)重新獲取一次Read View。

如表所示:

事務(wù)

說(shuō)明

begin;

select * from student where id >2;

獲取一次Read View

select * from student where id >2;

獲取一次Read View

commit;

注意,此時(shí)同樣的查詢語(yǔ)句都會(huì)重新獲取一次Read View,這時(shí)如果Read View 不同,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況。

當(dāng)隔離級(jí)別為可重復(fù)讀的時(shí)候,就避免了不可重復(fù)讀,這是因?yàn)橐粋€(gè)事務(wù)只在第一次SELECT的時(shí)候會(huì)獲取一次Read View,而后面所有的SELECT都會(huì)復(fù)用這個(gè)Read View,如下表所示:

事務(wù)

說(shuō)明

begin;

select * from student where id >2;

獲取一次Read View

select * from student where id >2;

commit;

4、MVCC示例

假設(shè)現(xiàn)在student表中只有一條由事務(wù)id為8的事務(wù)插入的一條記錄:



SELECT * FROM student ;
/*
+----+--------+--------+
| id | name | class |
+----+--------+--------+
| 1 | 張三 | 一班 |
+----+--------+--------+
1 row in set (0.07 sec)
*/

MVCC只能在READ COMMITTED和REPEATABLE READ兩個(gè)隔離級(jí)別下工作。READ COMMITTED和REPEATABLE READ生成ReadView的時(shí)機(jī)是不同的。

4.1 READ COMMITTED隔離級(jí)別


READ COMMITTED :每次讀取數(shù)據(jù)前都生成一個(gè)ReadView
現(xiàn)在有兩個(gè) 事務(wù)id 分別為 10 、 20 的事務(wù)在執(zhí)行
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

# Transaction 20
BEGIN;
# 更新了一些別的表的記錄(為了分配事務(wù)id)
...

說(shuō)明:事務(wù)執(zhí)行過(guò)程中,只有在第一次真正修改記錄時(shí)(比如使用INSERT、DELETE、UPDATE語(yǔ)句),才會(huì)被分配一個(gè)單獨(dú)的事務(wù)id,這個(gè)事務(wù)id是遞增的。所以我們才在事務(wù)20中更新一些別的表的記錄,目的是讓它分配事務(wù)id。

此刻,表student 中id為1的記錄得到的版本鏈表如下所示:

假設(shè)現(xiàn)在有一個(gè)使用 READ COMMITTED 隔離級(jí)別的事務(wù)開(kāi)始執(zhí)行:



# 使用READ COMMITTED隔離級(jí)別的事務(wù)

BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'

這個(gè)·SELECT1·的執(zhí)行過(guò)程如下:

步驟1:在執(zhí)行SELECT語(yǔ)句時(shí)會(huì)先生成一個(gè)ReadView ,ReadView的trx_ids列表的內(nèi)容就是[10,20],up_limit_id為10, low_limit_id為21, creator_trx_id為0。

步驟2:從版本鏈中挑選可見(jiàn)的記錄,從圖中看出,最新版本的列name的內(nèi)容是’王五’,該版本的trx_id值為10,在trx_ids列表內(nèi),所以不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本

步驟3:下一個(gè)版本的列name的內(nèi)容是’李四’,該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本

步驟4:下一個(gè)版本的列name的內(nèi)容是’張三’,該版本的trx_id值為8,小于ReadView中的up_limit_id值10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為‘張三’的記錄

之后,把 事務(wù)id 為 10 的事務(wù)提交一下:

# Transaction 10
BEGIN;

UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

COMMIT;

然后再到 事務(wù)id 為 20 的事務(wù)中更新一下表 student 中 id 為 1 的記錄:

# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE student SET name="錢(qián)七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 為 1 的記錄的版本鏈就長(zhǎng)這樣:

然后再到剛才使用 READ COMMITTED 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,如下:

# 使用READ COMMITTED隔離級(jí)別的事務(wù)
BEGIN;

# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'

# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'王五'

這個(gè)SELECT2的執(zhí)行過(guò)程如下:

步驟1∶在執(zhí)行SELECT語(yǔ)句時(shí)會(huì)又會(huì)單獨(dú)生成一個(gè)ReadView,該ReadView的trx_ids列表的內(nèi)容就是[20],up_limit_id為20,low_limit_id為21, creator_trx_id為0。

步驟2:從版本鏈中挑選可見(jiàn)的記錄,從圖中看出,最新版本的列name的內(nèi)容是’宋八’,該版本的trx_id值為20,在trx_ids列表內(nèi),所以不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本。

步驟3∶下一個(gè)版本的列name的內(nèi)容是‘錢(qián)七’,該版本的trx_id值為20,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本

步驟4∶下一個(gè)版本的列name的內(nèi)容是’王五’,該版本的trx_id值為10,小于ReadView中的up_limit_id值20,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為’王五’的記錄。

以此類(lèi)推,如果之后事務(wù)id為20的記錄也提交了,再次在使用READ COMMITED 隔離級(jí)別的事務(wù)查詢表student中id值為1的記錄時(shí),得到的結(jié)果就是'宋八'了,具體流程我們就不分析了。

強(qiáng)調(diào):使用READ COMMITTED隔離級(jí)別的事務(wù)在每次查詢開(kāi)始時(shí)都會(huì)生成一個(gè)獨(dú)立的ReadView

4.2 REPEATABLE READ隔離級(jí)別

使用 REPEATABLE READ 隔離級(jí)別的事務(wù)來(lái)說(shuō),只會(huì)在第一次執(zhí)行查詢語(yǔ)句時(shí)生成一個(gè) ReadView ,之后的查詢就不會(huì)重復(fù)生成了。

比如,系統(tǒng)里有兩個(gè) 事務(wù)id 分別為 10 、 20 的事務(wù)在執(zhí)行:



# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...

此刻,表student 中 id 為 1 的記錄得到的版本鏈表如下所示:

假設(shè)現(xiàn)在有一個(gè)使用 REPEATABLE READ 隔離級(jí)別的事務(wù)開(kāi)始執(zhí)行:


# 使用REPEATABLE READ隔離級(jí)別的事務(wù)
BEGIN;

# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'

這個(gè)SELECT1的執(zhí)行過(guò)程如下:

步驟1:在執(zhí)行·SELECT·語(yǔ)句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的trx_ids列表的內(nèi)容就是[10,20],up_limit_id為10, low_limit_id為21, creator_trx_id為0。

步驟2:然后從版本鏈中挑選可見(jiàn)的記錄,從圖中看出,最新版本的列name的內(nèi)容是’王五’,該版本的trx_id值為10,在trx_ids列表內(nèi),所以不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本。

步驟3:下一個(gè)版本的列name的內(nèi)容是’李四’,該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

步驟4∶下一個(gè)版本的列name的內(nèi)容是’張三’,該版本的trx_id值為8,小于ReadView中的up_limit_id值10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為’張三’的記錄

之后,我們把事務(wù)id為10的事務(wù)提交一下,就像這樣:


# Transaction 10
BEGIN;

UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

COMMIT;

然后再到 事務(wù)id 為 20 的事務(wù)中更新一下表 student 中 id 為 1 的記錄:

# Transaction 20
BEGIN;

# 更新了一些別的表的記錄
...
UPDATE student SET name="錢(qián)七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student 中 id 為 1 的記錄的版本鏈長(zhǎng)這樣:

然后再到剛才使用 REPEATABLE READ 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id 為 1 的記錄,如下:


# 使用REPEATABLE READ隔離級(jí)別的事務(wù)
BEGIN;

# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'

# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍為'張三'

SELECT2的執(zhí)行過(guò)程如下:

步驟1:因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為REPEATABLE READ,而之前在執(zhí)行SELECT1時(shí)已經(jīng)生成過(guò)ReadView了,所以此時(shí)直接復(fù)用之前的ReadView,之前的ReadView的trx_ids列表的內(nèi)容就是[10,20],up_limit_id為10,low_limit_id為21, creator_trx_id為0。

步驟2:然后從版本鏈中挑選可見(jiàn)的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是’宋八’,該版本的trx_id值為20,在trx_ids列表內(nèi),所以不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本

步驟3:下一個(gè)版本的列name的內(nèi)容是’錢(qián)七’,該版本的trx_id值為20,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本

步驟4∶下一個(gè)版本的列name的內(nèi)容是’王五’,該版本的trx_id值為10,而trx_ids列表中是包含值為10的事務(wù)id的,所以該版本也不符合要求,同理下一個(gè)列name的內(nèi)容是‘李四’的版本也不符合要求。繼續(xù)跳到下一個(gè)版本

步驟5:下一個(gè)版本的列name的內(nèi)容是’張三’,該版本的trx_id值為8,小于ReadView中的up_limit_id值10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列c為‘張三’的記錄。

兩次SELECT查詢得到的結(jié)果是重復(fù)的,記錄的列c值都是‘張三',這就是可重復(fù)讀的含義。如果我們之后再把事務(wù)id為20的記錄提交了,然后再到剛才使用REPEATABLE READ隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記得到的結(jié)果還是‘張三'。

4.3 如何解決幻讀

接下來(lái)說(shuō)明InnoDB 是如何解決幻讀的。

假設(shè)現(xiàn)在表 student 中只有一條數(shù)據(jù),數(shù)據(jù)內(nèi)容中,主鍵 id=1,隱藏的 trx_id=10,它的 undo log 如下圖所示。

假設(shè)現(xiàn)在有事務(wù) A 和事務(wù) B 并發(fā)執(zhí)行, 事務(wù) A 的事務(wù) id 為 20 , 事務(wù) B 的事務(wù) id 為 30 。

步驟1:事務(wù) A 開(kāi)始第一次查詢數(shù)據(jù),查詢的 SQL 語(yǔ)句如下

select * from student where id >= 1;

在開(kāi)始查詢之前,MySQL 會(huì)為事務(wù) A 產(chǎn)生一個(gè) ReadView,此時(shí) ReadView 的內(nèi)容如下:trx_ids=[20,30] up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。

由于此時(shí)表 student 中只有一條數(shù)據(jù),且符合 where id>=1 條件,因此會(huì)查詢出來(lái)。然后根據(jù) ReadView機(jī)制,發(fā)現(xiàn)該行數(shù)據(jù)的trx_id=10,小于事務(wù) A 的 ReadView 里 up_limit_id,這表示這條數(shù)據(jù)是事務(wù) A 開(kāi)啟之前,其他事務(wù)就已經(jīng)提交了的數(shù)據(jù),因此事務(wù) A 可以讀取到。

結(jié)論:事務(wù) A 的第一次查詢,能讀取到一條數(shù)據(jù),id=1。

步驟2:接著事務(wù) B(trx_id=30),往表 student 中新插入兩條數(shù)據(jù),并提交事務(wù)

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');

此時(shí)表student 中就有三條數(shù)據(jù)了,對(duì)應(yīng)的 undo 如下圖所示:

步驟3:接著事務(wù) A 開(kāi)啟第二次查詢,根據(jù)可重復(fù)讀隔離級(jí)別的規(guī)則,此時(shí)事務(wù) A 并不會(huì)再重新生成ReadView。此時(shí)表 student 中的 3 條數(shù)據(jù)都滿足 where id>=1 的條件,因此會(huì)先查出來(lái)。然后根據(jù)ReadView 機(jī)制,判斷每條數(shù)據(jù)是不是都可以被事務(wù) A 看到。

1)首先 id=1 的這條數(shù)據(jù),前面已經(jīng)說(shuō)過(guò)了,可以被事務(wù) A 看到。

2)然后是 id=2 的數(shù)據(jù),它的 trx_id=30,此時(shí)事務(wù) A 發(fā)現(xiàn),這個(gè)值處于 up_limit_id 和 low_limit_id 之間,因此還需要再判斷 30 是否處于 trx_ids 數(shù)組內(nèi)。由于事務(wù) A 的 trx_ids=[20,30],因此在數(shù)組內(nèi),這表示 id=2 的這條數(shù)據(jù)是與事務(wù) A 在同一時(shí)刻啟動(dòng)的其他事務(wù)提交的,所以這條數(shù)據(jù)不能讓事務(wù) A 看到

3)同理,id=3 的這條數(shù)據(jù),trx_id 也為 30,因此也不能被事務(wù) A 看見(jiàn)

結(jié)論:最終事務(wù) A 的第二次查詢,只能查詢出 id=1 的這條數(shù)據(jù)。這和事務(wù) A 的第一次查詢的結(jié)果是一樣的,因此沒(méi)有出現(xiàn)幻讀現(xiàn)象,所以說(shuō)在 MySQL 的可重復(fù)讀隔離級(jí)別下,不存在幻讀問(wèn)題。

5、總結(jié)

這里介紹了 MVCC 在 READ COMMITTD 、 REPEATABLE READ 這兩種隔離級(jí)別的事務(wù)在執(zhí)行快照讀操作時(shí)訪問(wèn)記錄的版本鏈的過(guò)程。這樣使不同事務(wù)的 讀-寫(xiě) 、 寫(xiě)-讀 操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。

核心點(diǎn)在于 ReadView 的原理, READ COMMITTD 、 REPEATABLE READ 這兩個(gè)隔離級(jí)別的一個(gè)很大不同就是生成ReadView的時(shí)機(jī)不同:

  • READ COMMITTD 在每一次進(jìn)行普通SELECT操作前都會(huì)生成一個(gè)ReadView
  • REPEATABLE READ只在第一次進(jìn)行普通SELECT操作前生成一個(gè)ReadView,之后的查詢操作都重復(fù)使用這個(gè)ReadView就好了

說(shuō)明:之前說(shuō)執(zhí)行DELETE語(yǔ)句或者更新主鍵的UPDATE語(yǔ)句并不會(huì)立即把對(duì)應(yīng)的記錄完全從頁(yè)面中刪除,而是執(zhí)行一個(gè)所謂的delete mark操作,相當(dāng)于只是對(duì)記錄打上了一個(gè)刪除標(biāo)志位,這主要就是為MVCC服務(wù)的。

通過(guò)MVCC 可以解決:

  • 讀寫(xiě)之間阻塞的問(wèn)題。通過(guò)MVCC 可以讓讀寫(xiě)互相不阻塞,即讀不阻塞寫(xiě),寫(xiě)不阻塞讀,這樣就可以提升事務(wù)并發(fā)處理能力
  • 降低了死鎖的概率。這是因?yàn)镸VCC采用了樂(lè)觀鎖的方式,讀取數(shù)據(jù)時(shí)并不需要加鎖,對(duì)于寫(xiě)操作,也只鎖定必要的行
  • 解決快照讀的問(wèn)題。當(dāng)查詢數(shù)據(jù)庫(kù)在某個(gè)時(shí)間點(diǎn)的快照時(shí),只能看到這個(gè)時(shí)間點(diǎn)之前事務(wù)提交更新的結(jié)果,而不能看到這個(gè)時(shí)間點(diǎn)之后事務(wù)提交的更新結(jié)果

本文題目:MySQLMVCC實(shí)現(xiàn)原理
當(dāng)前網(wǎng)址:http://www.dlmjj.cn/article/djdcsse.html