MySQL MVCC實現(xiàn)原理

1.概念

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

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

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

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

2.1 快照讀

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

SELECT*FROMplayerWHERE...復(fù)制代碼

之所以出現(xiàn)快照讀的情況凛驮,是基于提高并發(fā)性能的考慮裆站,快照讀的實現(xiàn)是基于MVCC,它在很多情況下,避免了加鎖操作遏插,降低了開銷捂贿。既然是基于多版本纠修,那么快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本胳嘲,而有可能是之前的歷史版本】鄄荩快照讀的前提是隔離級別不是串行級別了牛,串行級別下的快照讀會退化成當(dāng)前讀。

2.2 當(dāng)前讀

當(dāng)前讀讀取的是記錄的最新版本(最新數(shù)據(jù)辰妙,而不是歷史版本的數(shù)據(jù))鹰祸,讀取時還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會對讀取的記錄進(jìn)行加鎖密浑。加鎖的SELECT蛙婴,或者對數(shù)據(jù)進(jìn)行增刪改都會進(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實現(xiàn)

3.1 隱藏字段

對于使用InnoDB存儲引擎的表來說尔破,它的聚簇索引記錄中都包含兩個必要的隱藏列街图。

trx_id:每次一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,都會把該事務(wù)的事務(wù)id賦值給trx_id隱藏列懒构。

roll_pointer:每次對某條聚簇索引記錄進(jìn)行改動時餐济,都會把舊的版本寫入到undo日志中,然后這個隱藏列就相當(dāng)于一個指針胆剧,可以通過它來找到該記錄修改前的信息絮姆。

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ù)回滾時起作用秩霍,當(dāng)事務(wù)提交后篙悯,該類型的undo日志就沒用了,它占用的Undo Log Segment也會被系統(tǒng)回收(也就是該undo日志占用的Undo頁面鏈表要么被重用铃绒,要么被釋放)鸽照。

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


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

不能!這就是一個事務(wù)修改了另一個未提交事務(wù)修改過的數(shù)據(jù)移宅,臟寫。

InnoDB使用鎖來保證不會有臟寫情況的發(fā)生椿疗,也就是在第一個事務(wù)更新了某條記錄后漏峰,就會給這條記錄加鎖,另一個事務(wù)再次更新時就需要等待第一個事務(wù)提交了届榄,把鎖釋放之后才可以繼續(xù)更新浅乔。

每次對記錄進(jìn)行改動,都會記錄一條undo日志,每條undo日志也都有一個roll_pointer屬性(INSERT操作對應(yīng)的undo日志沒有該屬性靖苇,因為該記錄并沒有更早的版本)席噩,可以將這些undo日志都連起來,串成一個鏈表:


對該記錄每次更新后贤壁,都會將舊值放到一條undo日志中悼枢,就算是該記錄的一個舊版本,隨著更新次數(shù)的增多脾拆,所有的版本都會被roll_pointer屬性連接成一個鏈表馒索,把這個鏈表稱之為版本鏈,版本鏈的頭節(jié)點就是當(dāng)前記錄最新的值名船。每個版本中還包含生成該版本時對應(yīng)的事務(wù)id绰上。

3.3 ReadView

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

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

3.3.1 設(shè)計思路

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

使用SERIALIZABLE隔離級別的事務(wù),InnoDB規(guī)定使用加鎖的方式來訪問記錄丑掺。

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

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

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

說明:只有在對表中的記錄做改動時(執(zhí)行INSERT艳丛、DELETE匣掸、UPDATE這些語句時)才會為事務(wù)分配事務(wù)id,否則在一個只讀事務(wù)中的事務(wù)id值都默認(rèn)為0氮双。

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

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

low_limit_id ,表示生成ReadView時系統(tǒng)中應(yīng)該分配給下一個事務(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這三個事務(wù)乾忱,之后id為3的事務(wù)提交了。那么一個新的讀事務(wù)在生成ReadView時历极,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(如果之前沒有其他的新增事務(wù))图云,活躍的最小事務(wù)ID (up_limit_id)為trx2。


3.3.2 ReadView的規(guī)則

有了這個ReadView邻邮,這樣在訪問某條記錄時竣况,只需要按照下邊的步驟判斷記錄的某個版本是否可見。

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

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

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

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

如果在勋陪,說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問硫兰。

如果不在诅愚,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問劫映。

3.4 MVCC整體操作流程

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

首先獲取事務(wù)自己的版本號泳赋,也就是事務(wù) ID雌桑;

獲取 ReadView;

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

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

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

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

如表所示:


注意弛饭,此時同樣的查詢語句都會重新獲取一次Read View,這時如果Read View 不同萍歉,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況侣颂。

當(dāng)隔離級別為可重復(fù)讀的時候,就避免了不可重復(fù)讀枪孩,這是因為一個事務(wù)只在第一次SELECT的時候會獲取一次Read View憔晒,而后面所有的SELECT都會復(fù)用這個Read View藻肄,如下表所示:


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兩個隔離級別下工作。READ COMMITTED和REPEATABLE READ生成ReadView的時機(jī)是不同的拒担。

4.1 READ COMMITTED隔離級別

READ COMMITTED :每次讀取數(shù)據(jù)前都生成一個ReadView

現(xiàn)在有兩個 事務(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)


說明:事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(比如使用INSERT从撼、DELETE州弟、UPDATE語句),才會被分配一個單獨的事務(wù)id低零,這個事務(wù)id是遞增的婆翔。所以我們才在事務(wù)20中更新一些別的表的記錄,目的是讓它分配事務(wù)id掏婶。

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


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

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

BEGIN;

# SELECT1:Transaction 10、20未提交

SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'


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

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

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

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

步驟4:下一個版本的列name的內(nèi)容是’張三’,該版本的trx_id值為8干奢,小于ReadView中的up_limit_id值10痊焊,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列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="錢七" WHERE id=1;

UPDATE student SET name="宋八" WHERE id=1;


此刻薄啥,表student中 id 為 1 的記錄的版本鏈就長這樣:



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

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

# 使用READ COMMITTED隔離級別的事務(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í)行過程如下:

步驟1∶在執(zhí)行SELECT語句時會又會單獨生成一個ReadView绰寞,該ReadView的trx_ids列表的內(nèi)容就是[20]到逊,up_limit_id為20铣口,low_limit_id為21, creator_trx_id為0。

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

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

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

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

強(qiáng)調(diào):使用READ COMMITTED隔離級別的事務(wù)在每次查詢開始時都會生成一個獨立的ReadView

4.2 REPEATABLE READ隔離級別

使用 REPEATABLE READ 隔離級別的事務(wù)來說飒筑,只會在第一次執(zhí)行查詢語句時生成一個 ReadView 片吊,之后的查詢就不會重復(fù)生成了。

比如协屡,系統(tǒng)里有兩個 事務(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)在有一個使用 REPEATABLE READ 隔離級別的事務(wù)開始執(zhí)行:

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

BEGIN;

# SELECT1:Transaction 10肤晓、20未提交

SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'


這個SELECT1的執(zhí)行過程如下:

步驟1:在執(zhí)行·SELECT·語句時會先生成一個ReadView爷贫,ReadView的trx_ids列表的內(nèi)容就是[10,20]材原,up_limit_id為10, low_limit_id為21, creator_trx_id為0沸久。

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

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

步驟4∶下一個版本的列name的內(nèi)容是’張三’狭姨,該版本的trx_id值為8,小于ReadView中的up_limit_id值10苏遥,所以這個版本是符合要求的饼拍,最后返回給用戶的版本就是這條列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="錢七" WHERE id=1;

UPDATE student SET name="宋八" WHERE id=1;

此刻师抄,表student 中 id 為 1 的記錄的版本鏈長這樣:


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

# 使用REPEATABLE READ隔離級別的事務(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í)行過程如下:

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

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

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

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

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

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

4.3 如何解決幻讀

接下來說明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 開始第一次查詢數(shù)據(jù)女淑,查詢的 SQL 語句如下

select*fromstudentwhereid >=1;復(fù)制代碼

在開始查詢之前,MySQL 會為事務(wù) A 產(chǎn)生一個 ReadView辜御,此時 ReadView 的內(nèi)容如下:trx_ids=[20,30] up_limit_id=20 鸭你, low_limit_id=31 , creator_trx_id=20 擒权。

由于此時表 student 中只有一條數(shù)據(jù)袱巨,且符合 where id>=1 條件,因此會查詢出來碳抄。然后根據(jù) ReadView機(jī)制愉老,發(fā)現(xiàn)該行數(shù)據(jù)的trx_id=10,小于事務(wù) A 的 ReadView 里 up_limit_id剖效,這表示這條數(shù)據(jù)是事務(wù) A 開啟之前嫉入,其他事務(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,'王五');


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


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

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

2)然后是 id=2 的數(shù)據(jù)粉怕,它的 trx_id=30,此時事務(wù) A 發(fā)現(xiàn)抒巢,這個值處于 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 在同一時刻啟動的其他事務(wù)提交的,所以這條數(shù)據(jù)不能讓事務(wù) A 看到

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


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

5.總結(jié)

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

核心點在于 ReadView 的原理尿贫, READ COMMITTD 电媳、 REPEATABLE READ 這兩個隔離級別的一個很大不同就是生成ReadView的時機(jī)不同:

READ COMMITTD 在每一次進(jìn)行普通SELECT操作前都會生成一個ReadView

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

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

通過MVCC 可以解決:

1.讀寫之間阻塞的問題拼缝。通過MVCC 可以讓讀寫互相不阻塞娱局,即讀不阻塞寫,寫不阻塞讀咧七,這樣就可以提升事務(wù)并發(fā)處理能力

2.降低了死鎖的概率衰齐。這是因為MVCC采用了樂觀鎖的方式,讀取數(shù)據(jù)時并不需要加鎖继阻,對于寫操作耻涛,也只鎖定必要的行

3.解決快照讀的問題。當(dāng)查詢數(shù)據(jù)庫在某個時間點的快照時瘟檩,只能看到這個時間點之前事務(wù)提交更新的結(jié)果抹缕,而不能看到這個時間點之后事務(wù)提交的更新結(jié)果

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市墨辛,隨后出現(xiàn)的幾起案子卓研,更是在濱河造成了極大的恐慌,老刑警劉巖睹簇,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奏赘,死亡現(xiàn)場離奇詭異,居然都是意外死亡带膀,警方通過查閱死者的電腦和手機(jī)志珍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垛叨,“玉大人伦糯,你說我怎么就攤上這事∷栽” “怎么了敛纲?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長剂癌。 經(jīng)常有香客問我淤翔,道長,這世上最難降的妖魔是什么佩谷? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任旁壮,我火速辦了婚禮,結(jié)果婚禮上谐檀,老公的妹妹穿的比我還像新娘抡谐。我一直安慰自己,他們只是感情好桐猬,可當(dāng)我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布麦撵。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪免胃。 梳的紋絲不亂的頭發(fā)上音五,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天,我揣著相機(jī)與錄音羔沙,去河邊找鬼躺涝。 笑死,一個胖子當(dāng)著我的面吹牛撬碟,可吹牛的內(nèi)容都是我干的诞挨。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼呢蛤,長吁一口氣:“原來是場噩夢啊……” “哼惶傻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起其障,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤银室,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后励翼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜈敢,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年汽抚,在試婚紗的時候發(fā)現(xiàn)自己被綠了抓狭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡造烁,死狀恐怖否过,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惭蟋,我是刑警寧澤苗桂,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站告组,受9級特大地震影響煤伟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜木缝,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一便锨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧我碟,春花似錦鸿秆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恳守,卻和暖如春考婴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背催烘。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工沥阱, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伊群。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓考杉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舰始。 傳聞我的和親對象是個殘疾皇子崇棠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容