Mysql24之事務(wù)隔離級(jí)別和MVCC

[TOC]

事前準(zhǔn)備

為了故事的順利發(fā)展,我們需要?jiǎng)?chuàng)建一個(gè)表:

CREATE TABLE hero (
    number INT,
    name VARCHAR(100),
    country varchar(100),
    PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;

然后向這個(gè)表里插入一條數(shù)據(jù):

INSERT INTO hero VALUES(1, '劉備', '蜀');

現(xiàn)在表里的數(shù)據(jù)就是這樣的:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 劉備   | 蜀      |
+--------+--------+---------+
1 row in set (0.00 sec)

事務(wù)隔離級(jí)別

我們知道MySQL是一個(gè)客戶端/服務(wù)器架構(gòu)的軟件,對(duì)于同一個(gè)服務(wù)器來說,可以有若干個(gè)客戶端與之連接,每個(gè)客戶端與服務(wù)器連接上之后,就可以稱之為一個(gè)會(huì)話(Session)。每個(gè)客戶端都可以在自己的會(huì)話中向服務(wù)器發(fā)出請(qǐng)求語句,一個(gè)請(qǐng)求語句可能是某個(gè)事務(wù)的一部分,也就是對(duì)于服務(wù)器來說可能同時(shí)處理多個(gè)事務(wù)。在事務(wù)簡(jiǎn)介的章節(jié)中我們說過事務(wù)有一個(gè)稱之為隔離性的特性,理論上在某個(gè)事務(wù)對(duì)某個(gè)數(shù)據(jù)進(jìn)行訪問時(shí),其他事務(wù)應(yīng)該進(jìn)行排隊(duì),當(dāng)該事務(wù)提交之后,其他事務(wù)才可以繼續(xù)訪問這個(gè)數(shù)據(jù)。但是這樣子的話對(duì)性能影響太大,我們既想保持事務(wù)的隔離性,又想讓服務(wù)器在處理訪問同一數(shù)據(jù)的多個(gè)事務(wù)時(shí)性能盡量高些,魚和熊掌不可得兼,舍一部分隔離性而取性能者也。

事務(wù)并發(fā)執(zhí)行遇到的問題
怎么個(gè)舍棄法呢?我們先得看一下訪問相同數(shù)據(jù)的事務(wù)在不保證串行執(zhí)行(也就是執(zhí)行完一個(gè)再執(zhí)行另一個(gè))的情況下可能會(huì)出現(xiàn)哪些問題:

  • 臟寫(Dirty Write)

如果一個(gè)事務(wù)修改了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),那就意味著發(fā)生了臟寫,示意圖如下:


image.png

如上圖,Session A和Session B各開啟了一個(gè)事務(wù),Session B中的事務(wù)先將number列為1的記錄的name列更新為'關(guān)羽',然后Session A中的事務(wù)接著又把這條number列為1的記錄的name列更新為張飛。如果之后Session B中的事務(wù)進(jìn)行了回滾,那么Session A中的更新也將不復(fù)存在,這種現(xiàn)象就稱之為臟寫。這時(shí)Session A中的事務(wù)就很懵逼,我明明把數(shù)據(jù)更新了,最后也提交事務(wù)了,怎么到最后說自己啥也沒干呢?

  • 臟讀(Dirty Read)

如果一個(gè)事務(wù)讀到了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),那就意味著發(fā)生了臟讀,示意圖如下:

image.png

如上圖,Session A和Session B各開啟了一個(gè)事務(wù),Session B中的事務(wù)先將number列為1的記錄的name列更新為'關(guān)羽',然后Session A中的事務(wù)再去查詢這條number為1的記錄,如果du到列name的值為'關(guān)羽',而Session B中的事務(wù)稍后進(jìn)行了回滾,那么Session A中的事務(wù)相當(dāng)于讀到了一個(gè)不存在的數(shù)據(jù),這種現(xiàn)象就稱之為臟讀。

  • 不可重復(fù)讀(Non-Repeatable Read)

如果一個(gè)事務(wù)只能讀到另一個(gè)已經(jīng)提交的事務(wù)修改過的數(shù)據(jù),并且其他事務(wù)每對(duì)該數(shù)據(jù)進(jìn)行一次修改并提交后,該事務(wù)都能查詢得到最新值,那就意味著發(fā)生了不可重復(fù)讀,示意圖如下:

image.png

如上圖,我們?cè)赟ession B中提交了幾個(gè)隱式事務(wù)(注意是隱式事務(wù),意味著語句結(jié)束事務(wù)就提交了),這些事務(wù)都修改了number列為1的記錄的列name的值,每次事務(wù)提交之后,如果Session A中的事務(wù)都可以查看到最新的值,這種現(xiàn)象也被稱之為不可重復(fù)讀。

  • 幻讀(Phantom)
    如果一個(gè)事務(wù)先根據(jù)某些條件查詢出一些記錄,之后另一個(gè)事務(wù)又向表中插入了符合這些條件的記錄,原先的事務(wù)再次按照該條件查詢時(shí),能把另一個(gè)事務(wù)插入的記錄也讀出來,那就意味著發(fā)生了幻讀,示意圖如下:
image.png

如上圖,Session A中的事務(wù)先根據(jù)條件number > 0這個(gè)條件查詢表hero,得到了name列值為'劉備'的記錄;之后Session B中提交了一個(gè)隱式事務(wù),該事務(wù)向表hero中插入了一條新記錄;之后Session A中的事務(wù)再根據(jù)相同的條件number > 0查詢表hero,得到的結(jié)果集中包含Session B中的事務(wù)新插入的那條記錄,這種現(xiàn)象也被稱之為幻讀。

有的同學(xué)會(huì)有疑問,那如果Session B中是刪除了一些符合number > 0的記錄而不是插入新記錄,那Session A中之后再根據(jù)number > 0的條件讀取的記錄變少了,這種現(xiàn)象算不算幻讀呢?明確說一下,這種現(xiàn)象不屬于幻讀,幻讀強(qiáng)調(diào)的是一個(gè)事務(wù)按照某個(gè)相同條件多次讀取記錄時(shí),后讀取時(shí)讀到了之前沒有讀到的記錄。

小貼士: 那對(duì)于先前已經(jīng)讀到的記錄,之后又讀取不到這種情況,算啥呢?其實(shí)這相當(dāng)于對(duì)每一條記錄都發(fā)生了不可重復(fù)讀的現(xiàn)象。幻讀只是重點(diǎn)強(qiáng)調(diào)了讀取到了之前讀取沒有獲取到的記錄。

SQL標(biāo)準(zhǔn)中的四種隔離級(jí)別

我們上邊介紹了幾種并發(fā)事務(wù)執(zhí)行過程中可能遇到的一些問題,這些問題也有輕重緩急之分,我們給這些問題按照嚴(yán)重性來排一下序:

臟寫 > 臟讀 > 不可重復(fù)讀 > 幻讀

我們上邊所說的舍棄一部分隔離性來?yè)Q取一部分性能在這里就體現(xiàn)在:設(shè)立一些隔離級(jí)別,隔離級(jí)別越低,越嚴(yán)重的問題就越可能發(fā)生。有一幫人(并不是設(shè)計(jì)MySQL的大叔們)制定了一個(gè)所謂的SQL標(biāo)準(zhǔn),在標(biāo)準(zhǔn)中設(shè)立了4個(gè)隔離級(jí)別:

READ UNCOMMITTED:未提交讀。

READ COMMITTED:已提交讀。

REPEATABLE READ:可重復(fù)讀。

SERIALIZABLE:可串行化。

SQL標(biāo)準(zhǔn)中規(guī)定,針對(duì)不同的隔離級(jí)別,并發(fā)事務(wù)可以發(fā)生不同嚴(yán)重程度的問題,具體情況如下:

隔離級(jí)別    臟讀  不可重復(fù)讀   幻讀
READ UNCOMMITTED    Possible    Possible    Possible
READ COMMITTED  Not Possible    Possible    Possible
REPEATABLE READ Not Possible    Not Possible    Possible
SERIALIZABLE    Not Possible    Not Possible    Not Possible

MySQL中支持的四種隔離級(jí)別

不同的數(shù)據(jù)庫(kù)廠商對(duì)SQL標(biāo)準(zhǔn)中規(guī)定的四種隔離級(jí)別支持不一樣,比方說Oracle就只支持READ COMMITTED和SERIALIZABLE隔離級(jí)別。本書中所討論的MySQL雖然支持4種隔離級(jí)別,但與SQL標(biāo)準(zhǔn)中所規(guī)定的各級(jí)隔離級(jí)別允許發(fā)生的問題卻有些出入,MySQL在REPEATABLE READ隔離級(jí)別下,是可以禁止幻讀問題的發(fā)生的(關(guān)于如何禁止我們之后會(huì)詳細(xì)說明的)。

MySQL的默認(rèn)隔離級(jí)別為REPEATABLE READ,我們可以手動(dòng)修改一下事務(wù)的隔離級(jí)別。

如何設(shè)置事務(wù)的隔離級(jí)別

我們可以通過下邊的語句修改事務(wù)的隔離級(jí)別:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
其中的level可選值有4個(gè):

level: {
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
}

設(shè)置事務(wù)的隔離級(jí)別的語句中,在SET關(guān)鍵字后可以放置GLOBAL關(guān)鍵字、SESSION關(guān)鍵字或者什么都不放,這樣會(huì)對(duì)不同范圍的事務(wù)產(chǎn)生不同的影響,具體如下:

  • 使用GLOBAL關(guān)鍵字(在全局范圍影響):

比方說這樣:

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:

只對(duì)執(zhí)行完該語句之后產(chǎn)生的會(huì)話起作用。

當(dāng)前已經(jīng)存在的會(huì)話無效。

  • 使用SESSION關(guān)鍵字(在會(huì)話范圍影響):

比方說這樣:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:

對(duì)當(dāng)前會(huì)話的所有后續(xù)的事務(wù)有效

該語句可以在已經(jīng)開啟的事務(wù)中間執(zhí)行,但不會(huì)影響當(dāng)前正在執(zhí)行的事務(wù)。

如果在事務(wù)之間執(zhí)行,則對(duì)后續(xù)的事務(wù)有效。

  • 上述兩個(gè)關(guān)鍵字都不用(只對(duì)執(zhí)行語句后的下一個(gè)事務(wù)產(chǎn)生影響):

比方說這樣:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:

只對(duì)當(dāng)前會(huì)話中下一個(gè)即將開啟的事務(wù)有效。

下一個(gè)事務(wù)執(zhí)行完后,后續(xù)事務(wù)將恢復(fù)到之前的隔離級(jí)別。

該語句不能在已經(jīng)開啟的事務(wù)中間執(zhí)行,會(huì)報(bào)錯(cuò)的。

如果我們?cè)诜?wù)器啟動(dòng)時(shí)想改變事務(wù)的默認(rèn)隔離級(jí)別,可以修改啟動(dòng)參數(shù)transaction-isolation的值,比方說我們?cè)趩?dòng)服務(wù)器時(shí)指定了--transaction-isolation=SERIALIZABLE,那么事務(wù)的默認(rèn)隔離級(jí)別就從原來的REPEATABLE READ變成了SERIALIZABLE。

想要查看當(dāng)前會(huì)話默認(rèn)的隔離級(jí)別可以通過查看系統(tǒng)變量transaction_isolation的值來確定:

mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.02 sec)
或者使用更簡(jiǎn)便的寫法:

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

小貼士: 我們也可以使用設(shè)置系統(tǒng)變量transaction_isolation的方式來設(shè)置事務(wù)的隔離級(jí)別,不過我們前邊介紹過,一般系統(tǒng)變量只有GLOBAL和SESSION兩個(gè)作用范圍,而這個(gè)transaction_isolation卻有3個(gè)

MVCC原理

版本鏈
我們前邊說過,對(duì)于使用InnoDB存儲(chǔ)引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列(row_id并不是必要的,我們創(chuàng)建的表中有主鍵或者非NULL的UNIQUE鍵時(shí)都不會(huì)包含row_id列):

  • 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ì)把舊的版本寫入到undo日志中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過它來找到該記錄修改前的信息。

比方說我們的表hero現(xiàn)在只包含一條記錄:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 劉備   | 蜀      |
+--------+--------+---------+
1 row in set (0.07 sec)

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

image.png

小貼士: 實(shí)際上insert undo只在事務(wù)回滾時(shí)起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會(huì)被系統(tǒng)回收(也就是該undo日志占用的Undo頁(yè)面鏈表要么被重用,要么被釋放)。雖然真正的insert undo日志占用的存儲(chǔ)空間被釋放了,但是roll_pointer的值并不會(huì)被清除,roll_pointer屬性占用7個(gè)字節(jié),第一個(gè)比特位就標(biāo)記著它指向的undo日志的類型,如果該比特位的值為1時(shí),就代表著它指向的undo日志類型為insert undo。所以我們之后在畫圖時(shí)都會(huì)把insert undo給去掉,大家留意一下就好了。

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

image.png

小貼士: 能不能在兩個(gè)事務(wù)中交叉更新同一條記錄呢?哈哈,這不就是一個(gè)事務(wù)修改了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),淪為了臟寫了么?InnoDB使用鎖來保證不會(huì)有臟寫情況的發(fā)生,也就是在第一個(gè)事務(wù)更新了某條記錄后,就會(huì)給這條記錄加鎖,另一個(gè)事務(wù)再次更新時(shí)就需要等待第一個(gè)事務(wù)提交了,把鎖釋放之后才可以繼續(xù)更新。關(guān)于鎖的更多細(xì)節(jié)我們后續(xù)的文章中再嘮叨哈~

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

image.png

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

ReadView

對(duì)于使用READ UNCOMMITTED隔離級(jí)別的事務(wù)來說,由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了;對(duì)于使用SERIALIZABLE隔離級(jí)別的事務(wù)來說,設(shè)計(jì)InnoDB的大叔規(guī)定使用加鎖的方式來訪問記錄(加鎖是啥我們后續(xù)文章中說哈);對(duì)于使用READ COMMITTED和REPEATABLE READ隔離級(jí)別的事務(wù)來說,都必須保證讀到已經(jīng)提交了的事務(wù)修改過的記錄,也就是說假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見的。為此,設(shè)計(jì)InnoDB的大叔提出了一個(gè)ReadView的概念,這個(gè)ReadView中主要包含4個(gè)比較重要的內(nèi)容:

  • m_ids:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表。
  • min_trx_id:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的id值。

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

  • creator_trx_id:表示生成該ReadView的事務(wù)的事務(wù)id。

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

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

  • 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值大于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。

如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄。

在MySQL中,READ COMMITTED和REPEATABLE READ隔離級(jí)別的的一個(gè)非常大的區(qū)別就是它們生成ReadView的時(shí)機(jī)不同。我們還是以表hero為例來,假設(shè)現(xiàn)在表hero中只有一條由事務(wù)id為80的事務(wù)插入的一條記錄:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 劉備   | 蜀      |
+--------+--------+---------+
1 row in set (0.07 sec)

接下來看一下READ COMMITTED和REPEATABLE READ所謂的生成ReadView的時(shí)機(jī)不同到底不同在哪里。

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

比方說現(xiàn)在系統(tǒng)里有兩個(gè)事務(wù)id分別為100、200的事務(wù)在執(zhí)行:

# Transaction 100
BEGIN;

UPDATE hero SET name = '關(guān)羽' WHERE number = 1;

UPDATE hero SET name = '張飛' WHERE number = 1;
# Transaction 200
BEGIN;

小貼士: 再次強(qiáng)調(diào)一遍,事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(shí)(比如使用INSERT、DELETE、UPDATE語句),才會(huì)被分配一個(gè)單獨(dú)的事務(wù)id,這個(gè)事務(wù)id是遞增的。所以我們才在Transaction 200中更新一些別的表的記錄,目的是讓它分配事務(wù)id。

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

image.png

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

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

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

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

  • 在執(zhí)行SELECT語句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的m_ids列表的內(nèi)容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是'張飛',該版本的trx_id值為100,在m_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。

  • 下一個(gè)版本的列name的內(nèi)容是'關(guān)羽',該版本的trx_id值也為100,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

  • 下一個(gè)版本的列name的內(nèi)容是'劉備',該版本的trx_id值為80,小于ReadView中的min_trx_id值100,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為'劉備'的記錄。

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

# Transaction 100
BEGIN;

UPDATE hero SET name = '關(guān)羽' WHERE number = 1;

UPDATE hero SET name = '張飛' WHERE number = 1;

COMMIT;
然后再到事務(wù)id為200的事務(wù)中更新一下表hero中number為1的記錄:

# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...

UPDATE hero SET name = '趙云' WHERE number = 1;

UPDATE hero SET name = '諸葛亮' WHERE number = 1;

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

image.png

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

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

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張飛'

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

  • 在執(zhí)行SELECT語句時(shí)會(huì)又會(huì)單獨(dú)生成一個(gè)ReadView,該ReadView的m_ids列表的內(nèi)容就是[200](事務(wù)id為100的那個(gè)事務(wù)已經(jīng)提交了,所以再次生成快照時(shí)就沒有它了),min_trx_id為200,max_trx_id為201,creator_trx_id為0。
  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
  • 下一個(gè)版本的列name的內(nèi)容是'趙云',該版本的trx_id值為200,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
  • 下一個(gè)版本的列name的內(nèi)容是'張飛',該版本的trx_id值為100,小于ReadView中的min_trx_id值200,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為'張飛'的記錄。

以此類推,如果之后事務(wù)id為200的記錄也提交了,再此在使用READ COMMITTED隔離級(jí)別的事務(wù)中查詢表hero中number值為1的記錄時(shí),得到的結(jié)果就是'諸葛亮'了,具體流程我們就不分析了。總結(jié)一下就是:使用READ COMMITTED隔離級(jí)別的事務(wù)在每次查詢開始時(shí)都會(huì)生成一個(gè)獨(dú)立的ReadView。

REPEATABLE READ —— 在第一次讀取數(shù)據(jù)時(shí)生成一個(gè)ReadView

對(duì)于使用REPEATABLE READ隔離級(jí)別的事務(wù)來說,只會(huì)在第一次執(zhí)行查詢語句時(shí)生成一個(gè)ReadView,之后的查詢就不會(huì)重復(fù)生成了。我們還是用例子看一下是什么效果。

比方說現(xiàn)在系統(tǒng)里有兩個(gè)事務(wù)id分別為100、200的事務(wù)在執(zhí)行:

# Transaction 100
BEGIN;

UPDATE hero SET name = '關(guān)羽' WHERE number = 1;

UPDATE hero SET name = '張飛' WHERE number = 1;
# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...

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

image.png

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

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

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

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

  • 在執(zhí)行SELECT語句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的m_ids列表的內(nèi)容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。

  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是'張飛',該版本的trx_id值為100,在m_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。

  • 下一個(gè)版本的列name的內(nèi)容是'關(guān)羽',該版本的trx_id值也為100,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。

  • 下一個(gè)版本的列name的內(nèi)容是'劉備',該版本的trx_id值為80,小于ReadView中的min_trx_id值100,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為'劉備'的記錄。

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

# Transaction 100
BEGIN;

UPDATE hero SET name = '關(guān)羽' WHERE number = 1;

UPDATE hero SET name = '張飛' WHERE number = 1;

COMMIT;
然后再到事務(wù)id為200的事務(wù)中更新一下表hero中number為1的記錄:

# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...

UPDATE hero SET name = '趙云' WHERE number = 1;

UPDATE hero SET name = '諸葛亮' WHERE number = 1;

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

image.png

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

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

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍為'劉備'

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

  • 因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為REPEATABLE READ,而之前在執(zhí)行SELECT1時(shí)已經(jīng)生成過ReadView了,所以此時(shí)直接復(fù)用之前的ReadView,之前的ReadView的m_ids列表的內(nèi)容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
  • 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內(nèi)容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
  • 下一個(gè)版本的列name的內(nèi)容是'趙云',該版本的trx_id值為200,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
  • 下一個(gè)版本的列name的內(nèi)容是'張飛',該版本的trx_id值為100,而m_ids列表中是包含值為100的事務(wù)id的,所以該版本也不符合要求,同理下一個(gè)列name的內(nèi)容是'關(guān)羽'的版本也不符合要求。繼續(xù)跳到下一個(gè)版本。
  • 下一個(gè)版本的列name的內(nèi)容是'劉備',該版本的trx_id值為80,小于ReadView中的min_trx_id值100,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列c為'劉備'的記錄。

也就是說兩次SELECT查詢得到的結(jié)果是重復(fù)的,記錄的列c值都是'劉備',這就是可重復(fù)讀的含義。如果我們之后再把事務(wù)id為200的記錄提交了,然后再到剛才使用REPEATABLE READ隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)number為1的記錄,得到的結(jié)果還是'劉備',具體執(zhí)行過程大家可以自己分析一下。

MVCC小結(jié)

從上邊的描述中我們可以看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本并發(fā)控制)指的就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級(jí)別的事務(wù)在執(zhí)行普通的SEELCT操作時(shí)訪問記錄的版本鏈的過程,這樣子可以使不同事務(wù)的讀-寫、寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。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就好了。

關(guān)于purge

大家有沒有發(fā)現(xiàn)兩件事兒:

我們說insert undo在事務(wù)提交之后就可以被釋放掉了,而update undo由于還需要支持MVCC,不能立即刪除掉。

為了支持MVCC,對(duì)于delete mark操作來說,僅僅是在記錄上打一個(gè)刪除標(biāo)記,并沒有真正將它刪除掉。

隨著系統(tǒng)的運(yùn)行,在確定系統(tǒng)中包含最早產(chǎn)生的那個(gè)ReadView的事務(wù)不會(huì)再訪問某些update undo日志以及被打了刪除標(biāo)記的記錄后,有一個(gè)后臺(tái)運(yùn)行的purge線程會(huì)把它們真正的刪除掉

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評(píng)論 3 425
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,255評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,646評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,838評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,146評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,338評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,565評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評(píng)論 1 292
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,059評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,296評(píng)論 2 376