一文帶你了解MySQL之事務(wù)隔離級別和MVCC
一、數(shù)據(jù)準(zhǔn)備
為了我們學(xué)習(xí)的順利進(jìn)行,我們這邊創(chuàng)建一張 hero
表
CREATE TABLE hero( number INT PRIMARY KEY, name VARCHAR(4), country VARCHAR(2) );
這里需要注意的是,我們的 hero
表的主鍵是number
,而不是id
,主要是后邊要用到的事務(wù)id
做一下區(qū)別,然后我們給這個表里插入一條數(shù)據(jù)
mysql> INSERT INTO hero VALUES(1,'張角','東漢'); Query OK, 1 row affected (0.01 sec
現(xiàn)在我們表里的數(shù)據(jù)就是這樣:
mysql> SELECT * FROM hero; +--------+--------+---------+ | number | name | country | +--------+--------+---------+ | 1 | 張角 | 東漢 | +--------+--------+---------+ 1 row in set (0.00 sec)
二、事務(wù)隔離級別
我們知道MySQL
是CS
架構(gòu)的軟件,若干個客戶端與服務(wù)器連接上之后,就可以稱之為一個會話
(Session
)。每個客戶端都可以在自己的會話中向服務(wù)器發(fā)出請求語句,一個請求語句可能是某個事務(wù)的一部分,也就是對于服務(wù)器來說可能同時處理多個事務(wù)。在事務(wù)簡介的章節(jié)中我們說過事務(wù)有一個稱之為隔離性
的特性,理論上在某個事務(wù)對某個數(shù)據(jù)進(jìn)行訪問時,其他事務(wù)應(yīng)該進(jìn)行排隊,當(dāng)該事務(wù)提交之后,其他事務(wù)才可以繼續(xù)訪問這個數(shù)據(jù)。但是這樣子的話對性能影響太大,我們既想保持事務(wù)的隔離性
,又想讓服務(wù)器在處理訪問同一數(shù)據(jù)的多個事務(wù)時性能盡量高些,魚和熊掌不可得兼,舍一部分隔離性而取性能者也
。
2.1 事務(wù)并發(fā)執(zhí)行遇到的問題
怎么個舍棄法呢?我們先得看一下訪問相同數(shù)據(jù)的事務(wù)在不保證串行執(zhí)行
(也就是執(zhí)行完一個再執(zhí)行另一個)的情況下可能會出現(xiàn)哪些問題:
臟寫(Dirty Write):
如果一個事務(wù)修改了另一個未提交事務(wù)修改過的數(shù)據(jù)
,那就意味著發(fā)生了臟寫
,示意圖如下:
Session A | Session B | |
---|---|---|
1 | BEGIN; | |
2 | BEGIN; | |
3 | UPDATE hero SET name = ‘趙云’ WHERE number = 1; | |
4 | UPDATE hero SET name = ‘法正’ WHERE number = 1; | |
5 | COMMIT; | |
6 | ROLLBACK; |
如上表,Session A
和Session B
各開啟了一個事務(wù),Session B
中的事務(wù)先將number
列為1
的記錄的name
列更新為'趙云'
,然后Session A
中的事務(wù)接著又把這條number
列為1
的記錄的name
列更新為法正
。如果之后Session B
中的事務(wù)進(jìn)行了回滾,那么Session A
中的更新也將不復(fù)存在,這種現(xiàn)象就稱之為臟寫
。這時Session A
中的事務(wù)就很懵逼,我明明把數(shù)據(jù)更新了,最后也提交事務(wù)了,怎么到最后說自己啥也沒干呢?
臟讀(Dirty Read)
:如果一個事務(wù)讀到了另一個未提交事務(wù)修改過的數(shù)據(jù),那就意味著發(fā)生了臟讀,示意圖如下:
Session A | Session B | |
---|---|---|
1 | BEGIN; | |
2 | BEGIN; | |
3 | UPDATE hero SET name = ‘趙云’ WHERE number = 1; | |
4 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘趙云’,味的發(fā)生了臟讀 ) | |
5 | COMMIT; | |
6 | ROLLBACK; |
如上表,Session A
和Session B
各開啟了一個事務(wù),Session B
中的事務(wù)先將number
列為1
的記錄的name
列更新為'趙云'
,然后Session A
中的事務(wù)再去查詢這條number
為1
的記錄,如果讀到列name
的值為'趙云'
,而Session B
中的事務(wù)稍后進(jìn)行了回滾
,那么Session A
中的事務(wù)相當(dāng)于讀到了一個不存在的數(shù)據(jù)
,這種現(xiàn)象就稱之為臟讀
。
不可重復(fù)讀(Non-Repeatable Read):
如果一個事務(wù)只能讀到另一個已經(jīng)提交的事務(wù)修改過的數(shù)據(jù),并且其他事務(wù)每對該數(shù)據(jù)進(jìn)行一次修改并提交后,該事務(wù)都能查詢得到最新值,那就意味著發(fā)生了不可重復(fù)讀
,示意圖如下:
Session A | Session B | |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’) | |
3 | UPDATE hero SET name = ‘趙云’ WHERE number = 1; | |
4 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘趙云’,意味的發(fā)生了不可重復(fù)讀) | |
5 | UPDATE hero SET name = ‘法正’ WHERE number = 1; | |
6 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘法正’,意味的發(fā)生了不可重復(fù)讀) |
如上表,我們在Session B
中提交了幾個隱式事務(wù)
(意味著語句結(jié)束事務(wù)就提交了),這些事務(wù)都修改了number
列為1
的記錄的列name
的值,每次事務(wù)提交之后,如果Session A
中的事務(wù)都可以查看到最新的值,這種現(xiàn)象也被稱之為不可重復(fù)讀
。
幻讀(Phantom):
如果一個事務(wù)先根據(jù)某些條件查詢出一些記錄,之后另一個事務(wù)又向表中插入了符合這些條件的記錄,原先的事務(wù)再次按照該條件查詢時,能把另一個事務(wù)插入的記錄也讀出來,那就意味著發(fā)生了幻讀
,示意圖如下:
Session A | Session B | |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’) | |
3 | INSERT INTO hero VALUES(1,‘許諸’,‘魏國’); | |
4 | SELECT * FROM hero WHERE number = 1;(此時讀到的name列的值為‘張角’、‘許諸’,意味的發(fā)生了幻讀) |
如上表,Session A
中的事務(wù)先根據(jù)條件number > 0
這個條件查詢表hero
,得到了name
列值為'張角'
的記錄;之后Session B
中提交了一個隱式事務(wù)
,該事務(wù)向表hero
中插入了一條新記錄;之后Session A
中的事務(wù)再根據(jù)相同的條件number > 0
查詢表hero
,得到的結(jié)果集中包含Session B
中的事務(wù)新插入的那條記錄,這種現(xiàn)象也被稱之為幻讀
。
Q1
: 如果Session B
中是刪除了一些符合number > 0
的記錄而不是插入新記錄,那Session A
中之后再根據(jù)number > 0
的條件讀取的記錄變少了,這種現(xiàn)象算不算幻讀呢?
W1
: 明確說一下,這種現(xiàn)象不屬于幻讀,幻讀強(qiáng)調(diào)的是一個事務(wù)按照某個相同條件多次讀取記錄時,后讀取時讀到了之前沒有讀到的記錄。
小提士:
那對于先前已經(jīng)讀到的記錄,之后又讀取不到這種情況,算啥呢?其實這相當(dāng)于對每一條記錄都發(fā)生了不可重復(fù)讀的現(xiàn)象?;米x只是重點強(qiáng)調(diào)了讀取到了之前讀取沒有獲取到的記錄。
2.2 SQL標(biāo)準(zhǔn)中的四種隔離級別
我們上邊介紹了幾種并發(fā)事務(wù)執(zhí)行過程中可能遇到的一些問題,這些問題也有輕重緩急之分,我們給這些問題按照嚴(yán)重性來排一下序:
臟寫 > 臟讀 > 不可重復(fù)讀 > 幻讀
我們上邊所說的舍棄一部分隔離性來換取一部分性能在這里就體現(xiàn)在:設(shè)立一些隔離級別,隔離級別越低,越嚴(yán)重的問題就越可能發(fā)生。于是制定了一個所謂的SQL標(biāo)準(zhǔn),在標(biāo)準(zhǔn)中設(shè)立了4個隔離級別:
- READ UNCOMMITTED:未提交讀
- READ COMMITTED:已提交讀
- REPEATABLE READ:可重復(fù)讀
- SERIALIZABLE:可串行化
SQL標(biāo)準(zhǔn)
中規(guī)定,針對不同的隔離級別
,并發(fā)事務(wù)可以發(fā)生不同嚴(yán)重程度的問題,具體情況如下:
隔離級別 | 臟讀 | 不可重復(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 |
也就是說:
- READ UNCOMMITTED隔離級別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀問題。
- READ COMMITTED隔離級別下,可能發(fā)生不可重復(fù)讀和幻讀問題,但是不可以發(fā)生臟讀問題。
- REPEATABLE READ隔離級別下,可能發(fā)生幻讀問題,但是不可以發(fā)生臟讀和不可重復(fù)讀的問題。
- SERIALIZABLE隔離級別下,各種問題都不可以發(fā)生。
Q2:
臟寫是怎么回事兒?怎么里邊都沒寫呢?W2:
這是因為臟寫這個問題太嚴(yán)重了,不論是哪種隔離級別,都不允許臟寫的情況發(fā)生。
2.3 MySQL中支持的四種隔離級別
不同的數(shù)據(jù)庫廠商對SQL標(biāo)準(zhǔn)中規(guī)定的四種隔離級別支持不一樣,比方說Oracle就只支持READ COMMITTED和SERIALIZABLE隔離級別。MySQL雖然支持4種隔離級別,但與SQL標(biāo)準(zhǔn)中所規(guī)定的各級隔離級別允許發(fā)生的問題卻有些出入,MySQL在REPEATABLE READ隔離級別下,是可以禁止幻讀問題的發(fā)生的。
MySQL的默認(rèn)隔離級別為REPEATABLE READ,我們可以手動修改一下事務(wù)的隔離級別。
如何設(shè)置事務(wù)的隔離級別
我們可以通過下邊的語句修改事務(wù)的隔離級別:
SET [GLOBAL|SESSION|PERSIST] TRANSACTION ISOLATION LEVEL level;
其中的level
可選值有4
個:
level: { REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED | SERIALIZABLE }
設(shè)置事務(wù)的隔離級別的語句中,在SET
關(guān)鍵字后可以放置GLOBAL
關(guān)鍵字、SESSION
關(guān)鍵字、PERSIST
關(guān)鍵字或者什么都不放,這樣會對不同范圍的事務(wù)產(chǎn)生不同的影響,具體如下:
- 使用GLOBAL關(guān)鍵字(在全局范圍影響):
比如:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:
則:
- 只對執(zhí)行完該語句之后產(chǎn)生的會話起作用。
- 當(dāng)前已經(jīng)存在的會話無效
- 重啟之后會恢復(fù)到默認(rèn)值
使用
SESSION
關(guān)鍵字(在會話范圍影響):比如:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:
- 對當(dāng)前會話的所有后續(xù)的事務(wù)有效
- 該語句可以在已經(jīng)開啟的事務(wù)中間執(zhí)行,但不會影響當(dāng)前正在執(zhí)行的事務(wù)
- 如果在事務(wù)之間執(zhí)行,則對后續(xù)的事務(wù)有效
- 重啟之后會恢復(fù)到默認(rèn)值
使用
PERSIST
關(guān)鍵字(使全局系統(tǒng)變量變?yōu)橛谰眯裕?/p>
比如:SET PERSIST TRANSACTION ISOLATION LEVEL SERIALIZABLE;
則:
- 只對執(zhí)行完該語句之后產(chǎn)生的會話起作用
- 當(dāng)前已經(jīng)存在的會話無效
- 重啟之后不會恢復(fù)到默認(rèn)值
小提示:
在5.7版本以前在修改全局變量時候,并不會影響到配置文件,重啟之后就會恢復(fù)到默認(rèn)值,MySQL8可以使用PERSIST
使全局系統(tǒng)變量變?yōu)橛谰眯?/p>
上述三個關(guān)鍵字都不用(只對執(zhí)行語句后的下一個事務(wù)產(chǎn)生影響):
比如:SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
則:
- 只對當(dāng)前會話中下一個即將開啟的事務(wù)有效。
- 下一個事務(wù)執(zhí)行完后,后續(xù)事務(wù)將恢復(fù)到之前的隔離級別。
- 該語句不能在已經(jīng)開啟的事務(wù)中間執(zhí)行,會報錯的
想要查看當(dāng)前會話默認(rèn)的隔離級別可以通過查看系統(tǒng)變量transaction_isolation
的值來確定:
mysql> SHOW VARIABLES LIKE 'transaction_isolation'; +-----------------------+--------------+ | Variable_name | Value | +-----------------------+--------------+ | transaction_isolation | SERIALIZABLE | +-----------------------+--------------+ 1 row in set (0.01 sec)
或者使用更簡便的寫法:
mysql> SELECT @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | SERIALIZABLE | +-------------------------+ 1 row in set (0.00 sec)
三、MVCC
3.1 版本鏈
我們前邊說過,對于使用InnoDB
存儲引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id
并不是必要的,我們創(chuàng)建的表中有主鍵
或者非NULL的UNIQUE
鍵時都不會包含row_id
列):
trx_id
:每次一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,都會把該事務(wù)的事務(wù)id
賦值給trx_id
隱藏列。roll_pointer
:每次對某條聚簇索引記錄進(jìn)行改動時,都會把舊的版本寫入到undo
日志中,然后這個隱藏列就相當(dāng)于一個指針,可以通過它來找到該記錄修改前的信息
比如我們的表hero
現(xiàn)在只包含一條記錄:
mysql> SELECT * FROM hero; +--------+--------+---------+ | number | name | country | +--------+--------+---------+ | 1 | 張角 | 東漢 | +--------+--------+---------+ 1 row in set (0.00 sec)
假設(shè)插入該記錄的事務(wù)id
為80,那么此刻該條記錄的示意圖如下所示:
小提示:
實際上insert undo只在事務(wù)回滾時起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會被系統(tǒng)回收(也就是該undo日志占用的Undo頁面鏈表要么被重用,要么被釋放)。雖然真正的insert undo日志占用的存儲空間被釋放了,但是roll_pointer的值并不會被清除,roll_pointer屬性占用7個字節(jié),第一個比特位就標(biāo)記著它指向的undo日志的類型,如果該比特位的值為1時,就代表著它指向的undo日志類型為insert undo。所以我們之后在畫圖時都會把insert undo給去掉,大家留意一下就好了。
假設(shè)之后兩個事務(wù)id
分別為100
、200
的事務(wù)對這條記錄進(jìn)行UPDATE
操作,操作流程如下:
trx 100 | trx 200 | |
---|---|---|
1 | BEGIN; | |
2 | BEGIN; | |
3 | UPDATE hero SET name = ‘趙云’ WHERE number = 1; | |
4 | UPDATE hero SET name = ‘法正’ WHERE number = 1; | |
5 | COMMIT; | |
6 | UPDATE hero SET name = ‘孫尚香’ WHERE number = 1; | |
7 | UPDATE hero SET name = ‘妲己’ WHERE number = 1; | |
8 | COMMIT; |
小提示
:
能不能在兩個事務(wù)中交叉更新同一條記錄呢?哈哈,這不就是一個事務(wù)修改了另一個未提交事務(wù)修改過的數(shù)據(jù),淪為了臟寫了么?InnoDB使用鎖來保證不會有臟寫情況的發(fā)生,也就是在第一個事務(wù)更新了某條記錄后,就會給這條記錄加鎖,另一個事務(wù)再次更新時就需要等待第一個事務(wù)提交了,把鎖釋放之后才可以繼續(xù)更新。關(guān)于鎖的更多細(xì)節(jié)我們后續(xù)的文章中再進(jìn)行學(xué)習(xí)~
每次對記錄進(jìn)行更新,都會記錄一條undo
日志,每條undo
日志也都有一個roll_pointer
屬性(INSERT操作對應(yīng)的undo日志沒有該屬性,因為該記錄并沒有更早的版本),可以將這些undo
日志都連起來,串成一個鏈表,所以現(xiàn)在的情況就像下圖一樣:
對該記錄每次更新后,都會將舊值放到一條undo
日志中,就算是該記錄的一個舊版本,隨著更新次數(shù)的增多,所有的版本都會被roll_pointer
屬性連接成一個鏈表,我們把這個鏈表稱之為版本鏈
,版本鏈的頭節(jié)點就是當(dāng)前記錄最新的值。另外,每個版本中還包含生成該版本時對應(yīng)的事務(wù)id
,這個信息很重要,我們稍后就會用到。
3.2 ReadView
對于使用READ UNCOMMITTED
隔離級別的事務(wù)來說,由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了;對于使用SERIALIZABLE
隔離級別的事務(wù)來說,InnoDB
的規(guī)定使用加鎖的方式來訪問記錄
(加鎖是啥我們后續(xù)文章中說哈);對于使用READ COMMITTED
和REPEATABLE READ
隔離級別的事務(wù)來說,都必須保證讀到已經(jīng)提交了的事務(wù)修改過的記錄,也就是說假如另一個事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:需要判斷一下版本鏈中的哪個版本是當(dāng)前事務(wù)可見的。為此,InnoDB
提出了一個ReadView
的概念,這個ReadView
中主要包含4個比較重要的內(nèi)容:
m_ids
:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表。min_trx_id
:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,也就是m_ids中的最小值。max_trx_id
:表示生成ReadView時系統(tǒng)中應(yīng)該分配給下一個事務(wù)的id值。小提示:
注意max_trx_id并不是m_ids中的最大值,事務(wù)id是遞增分配的。比方說現(xiàn)在有id為1,2,3這三個事務(wù),之后id為3的事務(wù)提交了。那么一個新的讀事務(wù)在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。creator_trx_id:表示生成該ReadView的事務(wù)的事務(wù)id。
小提示:
我們前邊說過,只有在對表中的記錄做改動時(執(zhí)行INSERT、DELETE、UPDATE這些語句時)才會為事務(wù)分配事務(wù)id,否則在一個只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。
有了這個ReadView
,這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:
如果被訪問版本的
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
時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建ReadView
時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。
如果某個版本的數(shù)據(jù)對當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味著該條記錄對該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄。
在MySQL
中,READ COMMITTED
和REPEATABLE READ
隔離級別的的一個非常大的區(qū)別就是它們生成ReadView
的時機(jī)不同。我們還是以表hero
為例來,假設(shè)現(xiàn)在表hero
中只有一條由事務(wù)id
為80
的事務(wù)插入的一條記錄:
mysql> SELECT * FROM hero; +--------+--------+---------+ | number | name | country | +--------+--------+---------+ | 1 | 張角 | 東漢 | +--------+--------+---------+ 1 row in set (0.00 sec)
下來看一下READ COMMITTED
和REPEATABLE READ
所謂的生成ReadView
的時機(jī)不同到底不同在哪里
3.2.1 READ COMMITTED
對于使用REPEATABLE COMMITTED
隔離級別的事務(wù)來說,每次讀取數(shù)據(jù)前都生成一個ReadView
。比方說現(xiàn)在系統(tǒng)里有兩個事務(wù)id
分別為100
、200
的事務(wù)在執(zhí)行:
# Transaction 100 BEGIN; UPDATE hero SET name = '趙云' WHERE number = 1; UPDATE hero SET name = '法正' WHERE number = 1;
# Transaction 200 BEGIN; # 更新了一些別的表的記錄 ...
小提示:
再次強(qiáng)調(diào)一遍,事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(比如使用INSERT、DELETE、UPDATE語句),才會被分配一個單獨的事務(wù)id,這個事務(wù)id是遞增的。所以我們才在Transaction 200中更新一些別的表的記錄,目的是讓它分配事務(wù)id。
此刻,表hero
中number
為1
的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個使用READ COMMITTED
隔離級別的事務(wù)開始執(zhí)行:
# 使用READ COMMITTED隔離級別的事務(wù) BEGIN; # SELECT1:Transaction 100、200未提交 SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'
這個SELECT1
的執(zhí)行過程如下:
在執(zhí)行
SELECT
語句時會先生成一個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
跳到下一個版本。下一個版本的列
name
的內(nèi)容是'趙云'
,該版本的trx_id
值也為100
,也在m_ids
列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。下一個版本的列
name
的內(nèi)容是'張角'
,該版本的trx_id
值為80
,小于ReadView
中的min_trx_id
值100
,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name
為'張角'
的記錄。
# Transaction 100 BEGIN; UPDATE hero SET name = '趙云' 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
的記錄的版本鏈就長這樣:
然后再到剛才使用READ COMMITTED
隔離級別的事務(wù)中繼續(xù)查找這個number
為1
的記錄,如下:
# 使用READ COMMITTED隔離級別的事務(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的值為'法正'
這個SELECT2的執(zhí)行過程如下:
在執(zhí)行
SELECT
語句時會又會單獨生成一個ReadView
,該ReadView
的m_ids
列表的內(nèi)容就是[200]
(事務(wù)id
為100
的那個事務(wù)已經(jīng)提交了,所以再次生成快照時就沒有它了),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
跳到下一個版本。下一個版本的列
name
的內(nèi)容是'孫尚香'
,該版本的trx_id
值為200
,也在m_ids
列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。下一個版本的列
name
的內(nèi)容是'法正'
,該版本的trx_id
值為100
,小于ReadView
中的min_trx_id
值200
,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列name
為'法正'
的記錄。
以此類推,如果之后事務(wù)id
為200
的記錄也提交了,再次在使用READ COMMITTED
隔離級別的事務(wù)中查詢表hero
中number
值為1
的記錄時,得到的結(jié)果就是'妲己'
了,具體流程我們就不分析了??偨Y(jié)一下就是:使用READ COMMITTED
隔離級別的事務(wù)在每次查詢開始時都會生成一個獨立的ReadView
。
3.2.2 REPEATABLE READ
對于使用REPEATABLE READ
隔離級別的事務(wù)來說,只會在第一次執(zhí)行查詢語句時生成一個ReadView
,之后的查詢就不會重復(fù)生成了。比方說現(xiàn)在系統(tǒng)里有兩個事務(wù)id
分別為100
、200
的事務(wù)在執(zhí)行:
# Transaction 100 BEGIN; UPDATE hero SET name = '趙云' WHERE number = 1; UPDATE hero SET name = '法正' WHERE number = 1;
# Transaction 200 BEGIN; # 更新了一些別的表的記錄 ...
此刻,表hero
中number
為1
的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個使用REPEATABLE READ
隔離級別的事務(wù)開始執(zhí)行:
# 使用REPEATABLE READ隔離級別的事務(wù) BEGIN; # SELECT1:Transaction 100、200未提交 SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張角'
這個SELECT1
的執(zhí)行過程如下:
在執(zhí)行
SELECT
語句時會先生成一個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
跳到下一個版本。下一個版本的列
name
的內(nèi)容是'趙云'
,該版本的trx_id
值也為100
,也在m_ids
列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。下一個版本的列
name
的內(nèi)容是'張角'
,該版本的trx_id
值為80
,小于ReadView
中的min_trx_id
值100
,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列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
的記錄的版本鏈就長這樣:
然后再到剛才使用REPEATABLE READ
隔離級別的事務(wù)中繼續(xù)查找這個number
為1
的記錄,如下:
# 使用REPEATABLE READ隔離級別的事務(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的值仍為'張角'
這個SELECT2
的執(zhí)行過程如下:
因為當(dāng)前事務(wù)的隔離級別為
REPEATABLE READ
,而之前在執(zhí)行SELECT1
時已經(jīng)生成過ReadView
了,所以此時直接復(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
跳到下一個版本。下一個版本的列
name
的內(nèi)容是'孫尚香'
,該版本的trx_id
值為200
,也在m_ids
列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個版本。下一個版本的列name的內(nèi)容是
'法正'
,該版本的trx_id
值為100
,而m_ids
列表中是包含值為100
的事務(wù)id
的,所以該版本也不符合要求,同理下一個列name
的內(nèi)容是'趙云'
的版本也不符合要求。繼續(xù)跳到下一個版本。下一個版本的列
name
的內(nèi)容是'張角'
,該版本的trx_id
值為80
,小于ReadView
中的min_trx_id
值100
,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列‘name’
為'張角'
的記錄。
也就是說兩次SELECT
查詢得到的結(jié)果是重復(fù)
的,記錄的列name
值都是'張角'
,這就是可重復(fù)讀
的含義。如果我們之后再把事務(wù)id
為200
的記錄提交了,然后再到剛才使用REPEATABLE READ
隔離級別的事務(wù)中繼續(xù)查找這個number
為1
的記錄,得到的結(jié)果還是'張角'
,具體執(zhí)行過程大家可以自己分析一下。
3.3 MVCC小結(jié)
從上邊的描述中我們可以看出來,所謂的MVCC
(Multi-Version Concurrency Control ,多版本并發(fā)控制
)指的就是在使用READ COMMITTD
、REPEATABLE READ
這兩種隔離級別的事務(wù)在執(zhí)行普通的SELECT
操作時訪問記錄的版本鏈的過程,這樣子可以使不同事務(wù)的讀-寫
、寫-讀
操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。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只是在我們進(jìn)行普通的SEELCT查詢時才生效,截止到目前我們所見的所有SELECT語句都算是普通的查詢,至于啥是個不普通的查詢,我們稍后再說哈~
四、關(guān)于purge
大家有沒有發(fā)現(xiàn)兩件事兒:
我們說
insert undo
在事務(wù)提交之后就可以被釋放掉了,而update undo
由于還需要支持MVCC
,不能立即刪除掉。我們在學(xué)習(xí)
undo
日志的時候,一個事務(wù)寫的一組undo
日志中都有一個Undo Log Heade
r部分,這個Undo Log Header
中有一個名為TRX_UNDO_HISTORY_NODE
的屬性,表示名為History鏈表的節(jié)點
。當(dāng)一個事務(wù)提交之后,就會把這個事務(wù)的執(zhí)行過程產(chǎn)生的這一組Update undo
日志插入到History鏈表的頭部
我們還說過,每個回滾段都對應(yīng)一個名為
Rollbcak Segment Header
的頁面,這個頁面中有兩個屬性:TRX_RSEG_HISTORY
:表示Histroy鏈表的基節(jié)點TRX_RSEG_HISTORY_SIZE
:表示Histroy鏈表占用的頁面數(shù)量
也就是說每個回滾段都有一個
History鏈表
,一個事務(wù)在某個回滾段中寫入的一組update undo
日志在該事物提交之后就會加入這個回滾段的History
鏈表。系統(tǒng)中存在很多回滾段,這也意味著可能存在很多個History
鏈表。不過加入到History鏈表的update undo
日志所占用的存儲空間頁沒有釋放掉,他們總不能一直存在吧,那得用多大的存儲空間存放這些undo
日志。為了支持
MVCC
,對于delete mark
操作來說,僅僅是在記錄上打一個刪除標(biāo)記,并沒有真正將它刪除掉。大家應(yīng)該還記得,在一組
undo
日志中的Undo Log Header
部分有個名為TRX_UNDO_DEL_MARKS
的屬性,用來標(biāo)記本組的undo
日志中是否包含因delete mark
操作二產(chǎn)生的日志,這些打了刪除標(biāo)記的記錄也不能一直存在吧?
為了節(jié)約空間,我們應(yīng)該在合適的時候把update undo
日志以及僅僅被標(biāo)記為刪除的記錄
徹底刪除,這個刪除操作就是purge
。不過于問題的關(guān)健在于這個合適的時候到底是什么時候
?
update undo
日志和被標(biāo)記刪除的記錄都是為了支持MVCC
而存在的,只要系統(tǒng)中最早產(chǎn)生的ReadView
不在訪問它們,它們的使命就此結(jié)束了,就可以丟進(jìn)歷史的垃圾堆里了。一個ReadView
在什么時候才肯定不會訪問某個事物執(zhí)行過程中產(chǎn)生的undo日志呢?其實,只要我們能保證生成ReadView
時,某個事物已經(jīng)提交,那么該ReadView
肯定就不需要訪問事物運(yùn)行過程中產(chǎn)生的undo
日志了(因為該事物所改動的最新版均對該ReadView
可見)。
InnoDB
為此做了兩件事:
在一個事務(wù)提交時,會為這個事務(wù)生成一個名為
事務(wù)no
的值,該值用來表示事務(wù)提交的順序。先提交的事務(wù)的事務(wù)no
值小,后提交的事務(wù)的事務(wù)no
值大。別忘看在一組undo日志中對應(yīng)的
Undo Log Header
部分有一個名為TRX_UNDO_TRX_NO
的屬性。當(dāng)事務(wù)提交時,就把該事物對應(yīng)的事務(wù)no值填到這個屬性中,因為事務(wù)no
代表著各個事務(wù)提交的順序,而History鏈表
又是按照事務(wù)提交的順序來排列各種undo
日志,所以History鏈表
中各組的undo
日志也是按照對應(yīng)的事務(wù)no
來排序的。一個
ReadView
結(jié)構(gòu)除了包含前面學(xué)習(xí)的幾個屬性之外,還會包含一個事務(wù)no
的屬性。在生成一個ReadView
時,會把比當(dāng)前系統(tǒng)中最大的事務(wù)no
的值還大1的值賦予這個屬性。
InnoDB
還把當(dāng)前系統(tǒng)中所有的ReadView
按照創(chuàng)建時間連成了一個鏈表。當(dāng)執(zhí)行purge
操作時(這個purge操作是在專門的后線程中執(zhí)行的),就把系統(tǒng)中最早生成的ReadView
給取出來。如果當(dāng)前系統(tǒng)中不存在ReadView
,那就現(xiàn)場創(chuàng)建一個(新創(chuàng)建的這個ReadView的事務(wù)no值肯定比當(dāng)前已經(jīng)提交的事務(wù)的事務(wù)no值大)。然后從各個回滾段的History鏈表
中取出事務(wù)no
值較小的各組undo
日志。如果第一組undo
日志的事務(wù)no
小于當(dāng)前系統(tǒng)最早生成的ReadView
的事務(wù)no
,就意味著該組undo
日志沒有用了,就會從History鏈表
中移除,并且釋放掉它們占用的存儲空間。如果該組undo
日志包含因delete mark
操作而產(chǎn)生的undo
日志(TRX_UNDO_DEL_MARKS的屬性值為1
),那么也需要將對應(yīng)的標(biāo)記為刪除的記錄給徹底刪除。
這里我們需要注意的一點,當(dāng)前系統(tǒng)中系統(tǒng)最早生成的ReadView
決定了purge
操作中可以清理哪些update undo
日志以及打了刪除標(biāo)記
的記錄。如果某個事物使用的REPEATABLE READ
隔離級別,那么該事物一直復(fù)用最初產(chǎn)生的ReadView
。假如這個事務(wù)運(yùn)行了很久,一直沒有提交,那么最早產(chǎn)生的ReadView
會一直不釋放。系統(tǒng)中updtae undo
日志和打了刪除標(biāo)記的記錄
就會越來越多,表空間對應(yīng)的文件也會越來越大,一條記錄的版本鏈將越來越長,從而影響系統(tǒng)的性能。
五、總結(jié)
并發(fā)的事務(wù)在運(yùn)行過程中會出現(xiàn)一些可能引發(fā)一致性問題的現(xiàn)象,具體如下:
臟寫:
一個事務(wù)修改了另一個未提交事務(wù)修改過的數(shù)據(jù)臟讀
:一個事務(wù)讀到了另一個未提交事務(wù)修改過的數(shù)據(jù)不可重復(fù)讀:
一個事務(wù)修改了另一個未提交事務(wù)讀取的數(shù)據(jù)幻讀:
一個事務(wù)先根據(jù)某些搜索條件查詢出一些記錄,在該事務(wù)未提交時,另一個事務(wù)寫入了一些符合那些搜索條件的記錄
SOL標(biāo)準(zhǔn)中的4種隔離級別:
READ UNCOMMITTED:
可能發(fā)生臟讀、不可重復(fù)讀和幻讀現(xiàn)象READCOMMITTED:
可能發(fā)生不可重復(fù)讀和幻讀現(xiàn)象,但是不可能發(fā)生臟讀現(xiàn)象。REPEATABLE READ:
可能發(fā)生幻讀現(xiàn)象,但是不可能發(fā)生臟讀和不可重復(fù)讀的現(xiàn)象SERIALIZABLE:
各種現(xiàn)象都不可以發(fā)生。
實際上,
MySOL
在REPEATABLE READ
隔離級別下是可以在很大程度上禁止出現(xiàn)幻讀現(xiàn)象的。下面的語句用來設(shè)置事務(wù)的隔離級別:
SET [GLOBALI|SESSION|PERSIST] TRANSACTION ISOLATION LEVEL Level;
聚簇索引記錄和
undo
日志中的roll pointer
屬性可以串連成一個記錄的版本鏈
通過生成
ReadView
來判斷記錄的某個版本的可見性,其中READ COMMITTD
在每一次進(jìn)行普通SELECT操作前都會生成一個ReadView
,而REPEATABLE READ
只在第一次進(jìn)行普通SELECT
操作前生成一個ReadView
,之后的查詢操作都重復(fù)使用這個ReadView
當(dāng)前系統(tǒng)中,如果最早生成的
ReadView
不再訪問undo日志
以及打了刪除標(biāo)記的記錄
則可以通過purge
操作將它們清除。
以上就是一文帶你了解MySQL之事務(wù)隔離級別和MVCC的詳細(xì)內(nèi)容,更多關(guān)于MySQL 事務(wù)隔離級別和MVCC的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MySQL通過日志恢復(fù)數(shù)據(jù)的操作步驟
在MySQL中恢復(fù)誤刪除的數(shù)據(jù)是一個常見但復(fù)雜的問題,具體的方法取決于幾個因素,包括您是否有備份、使用的是哪種存儲引擎(如InnoDB或MyISAM)、以及您的數(shù)據(jù)庫配置等,本文給大家介紹了MySQL通過日志恢復(fù)數(shù)據(jù)的操作步驟,需要的朋友可以參考下2024-12-12MySQL復(fù)制的概述、安裝、故障、技巧、工具(火丁分享)
首先主服務(wù)器把數(shù)據(jù)變化記錄到主日志,然后從服務(wù)器通過I/O線程讀取主服務(wù)器上的主日志,并且把它寫入到從服務(wù)器的中繼日志中,接著SQL線程讀取中繼日志,并且在從服務(wù)器上重放,從而實現(xiàn)MySQL復(fù)制。2011-04-04Mysql調(diào)優(yōu)Explain工具詳解及實戰(zhàn)演練(推薦)
這篇文章主要介紹了Mysql調(diào)優(yōu)Explain工具詳解及實戰(zhàn)演練,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03MySQL中l(wèi)ength()、char_length()的區(qū)別
在MySQL中l(wèi)ength(str)、char_length(str)都屬于判斷長度的內(nèi)置函數(shù),本文主要介紹了MySQL中l(wèi)ength()、char_length()的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05