MYSQL事務(wù)的隔離級別與MVCC
前言
提到數(shù)據(jù)庫,你多半會聯(lián)想到事務(wù),進(jìn)而還可能想起曾經(jīng)背得滾瓜亂熟的ACID,不知道你有沒有想過這個問題,事務(wù)有原子性、隔離性、一致性和持久性四大特性,為什么偏偏給隔離性設(shè)置了級別?一切還得從事務(wù)說起。
1. 事務(wù)(transaction)的起源
學(xué)習(xí)數(shù)據(jù)庫事務(wù)的時候,一個典型的案例就是「轉(zhuǎn)賬」,這篇文章也不能免俗,故事就從招財向陀螺借100塊錢開始吧。
一個看似非常簡單的現(xiàn)實世界的狀態(tài)轉(zhuǎn)換,轉(zhuǎn)換成數(shù)據(jù)庫中的操作卻并沒有那么單純。
這個看起來很簡單的借錢操作至少包含了兩個動作:
- 陀螺的賬戶余額-100
- 招財?shù)馁~戶余額+100
要保證轉(zhuǎn)賬操作的成功,數(shù)據(jù)庫必須把這兩個操作作為一個邏輯整體來執(zhí)行,這個邏輯整體就是一個事務(wù)。
1.1. 事務(wù)的定義
事務(wù)就是包含有限個(單條或多條)數(shù)據(jù)庫操作(增刪改查)的、最小的邏輯工作單元(不可再分)。
說到這里不得不吐槽一下,事務(wù)的英文是transaction,直譯為“交易”的意思,但是不知道為什么被意譯成了“事務(wù)”,讓人很難從字面上理解這個概念的含義。
中國人對翻譯的“信達(dá)雅”的偏執(zhí)在計算機領(lǐng)域或多或少有點不討喜。
1.2. 哪些存儲引擎支持事務(wù)
并不是所有的數(shù)據(jù)庫或者所有的存儲引擎都支持事務(wù)。
對于MySQL而言,事務(wù)作為一種功能特性由存儲引擎提供。目前支持事務(wù)功能的存儲引擎只有InnoDB
和NDB
,鑒于InnoDB
目前是MySQL默認(rèn)的存儲引擎,我們的研究重點自然也就是InnoDB
存儲引擎了。
因此文章接下來默認(rèn)的存儲引擎就是InnoDB
,特殊情況下會特別指出。
那么InnoDB
在什么情況下才會出現(xiàn)事務(wù)呢?
2. MySQL的事務(wù)語法
如果你不是DBA,在平時和MySQL的交互中你可能極少直接使用到它的事務(wù)語法,一切都被編程框架封裝得很好了。但是現(xiàn)在我們要直接使用MySQL進(jìn)行事務(wù)的研究了,拋開框架,跟我稍微回顧一下語法,這是非常必要的。
2.1. 自動提交
當(dāng)我運行這樣單獨一條更新語句的時候,它會有事務(wù)嗎?
UPDATE user_innodb SET name = '蟬沐風(fēng)' WHERE id = 1;
實際上,這條語句不僅會自動開啟一個事務(wù),而且執(zhí)行完畢之后還會自動提交事務(wù),并持久化數(shù)據(jù)。
這是MySQL默認(rèn)情況下使用的方式——自動提交。在此方式下,增刪改的SQL語句會自動開啟事務(wù),并且是一條SQL一個事務(wù)。
自動提交的方式雖然簡單,但是對于轉(zhuǎn)賬這種涉及到多條SQL的業(yè)務(wù),就不太適合了。因此,MySQL提供了手動開啟事務(wù)的方法。
2.2. 手動操作事務(wù)
2.2.1. 開啟事務(wù)
可以使用下面兩種語句開啟一個事務(wù):
BEGIN
START TRANSACTION
對比
BEGIN
而言,START TRANSACTION
后面可以添加一些操作符,不過這不是我們的研究重點,可以不必理會。
2.2.2. 提交或回滾
開啟事務(wù)之后就可以繼續(xù)編寫需要放到當(dāng)前事務(wù)中的SQL語句了。當(dāng)寫完最后一條語句,如果你覺得寫得沒問題,你可以提交事務(wù);反之你后悔了,想把數(shù)據(jù)庫恢復(fù)到之前的狀態(tài),你可以回滾事務(wù)。
- 提交事務(wù)
COMMIT
- 回滾事務(wù)
ROLLBACK
2.3. autocommit系統(tǒng)變量
MySQL提供了一個叫做autocommit
的系統(tǒng)變量,用來表示是否開啟自動提交:
mysql> SHOW VARIABLES LIKE 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+
autocommit
的默認(rèn)值為ON
,表示默認(rèn)開啟自動提交。但是自動提交和手動操作事務(wù)并不沖突,如果不顯式使用BEGIN
或START TRANSACTION
開啟一個事務(wù),那么InnoDB會在每一條增刪改語句執(zhí)行之后提交事務(wù)。
如果我們把autocommit
設(shè)為OFF
,除非我們手動使用BEGIN
或START TRANSACTION
開啟一個事務(wù),否則InnoDB絕不會自動開啟事務(wù);同樣,除非我們使用COMMIT
或ROLLBACK
提交或回滾事務(wù),否則InnoDB不會自動結(jié)束事務(wù)。
實際上,InnoDB會因為某些特殊語句的執(zhí)行或客戶端連接斷開等特殊情況而導(dǎo)致事務(wù)自動提交(即使我們沒有手動輸入
COMMIT
),這種情況叫做隱式提交。
3. 事務(wù)并發(fā)執(zhí)行導(dǎo)致的讀問題
MySQL會使用獨立的線程處理每一個客戶端的連接,這就是多線程。每個線程都可以開啟事務(wù),這就是事務(wù)的并發(fā)。
不管是多線程的并發(fā)執(zhí)行還是事務(wù)的并發(fā)執(zhí)行(其實本質(zhì)上是一回事兒),如果不采取點措施,都會帶來一些問題。
3.1. 臟讀
假設(shè)事務(wù)T1
和T2
并發(fā)執(zhí)行,都要訪問user_innodb
表中id
為1
的數(shù)據(jù),不同的是T1
先讀取數(shù)據(jù),緊接著T2
修改了數(shù)據(jù)的name
字段,需要注意的是,T2
并沒有提交!
此時,T1
再次執(zhí)行相同的查詢操作,會發(fā)現(xiàn)數(shù)據(jù)發(fā)生了變化,name
字段由「王剛蛋」變成了「蟬沐風(fēng)」。
如果一個事務(wù)讀到了另一個未提交事務(wù)修改過的數(shù)據(jù),而導(dǎo)致了前后兩次讀取的數(shù)據(jù)不一致的情況,這種事務(wù)并發(fā)問題叫做臟讀。
3.2. 不可重復(fù)讀
同樣是T1
和T2
兩個事務(wù),T1
通過id=1
查詢到了一條數(shù)據(jù),然后T2
緊接著UPDATE
(DELETE
也可以)了該條記錄,不同的是,T2
緊接著通過COMMIT
提交了事務(wù)。
此時,T1
再次執(zhí)行相同的查詢操作,會發(fā)現(xiàn)數(shù)據(jù)發(fā)生了變化,name
字段由「王剛蛋」變成了「蟬沐風(fēng)」。
如果一個事務(wù)讀到了另一個已提交事務(wù)修改過的(或者是刪除的)數(shù)據(jù),而導(dǎo)致了前后兩次讀取的數(shù)據(jù)不一致的情況,這種事務(wù)并發(fā)問題叫做不可重復(fù)讀。
看到這里是不是有點懵了?怎么讀到未提交事務(wù)修改的數(shù)據(jù)是并發(fā)問題,讀到已提交事務(wù)修改的數(shù)據(jù)還是并發(fā)問題呢?
這里先不急著回答你,因為還有個幻讀呢。
3.3. 幻讀
還是T1
和T2
這倆貨,T1
先查找了所有name
為「王剛蛋」的用戶信息,此時發(fā)現(xiàn)擁有這個硬漢名字的用戶只有一個。然后T2
插入了一個同樣叫做「王剛蛋」的用戶的信息,并且提交了。
此時,T1
再次執(zhí)行相同的查詢操作,發(fā)現(xiàn)相比上次的查詢結(jié)果多了一行數(shù)據(jù),不由得懷疑自己是不是出了幻覺。
如果一個事務(wù)首先根據(jù)某些搜索條件P查詢出一些結(jié)果,另一個事務(wù)寫入(可以是INSERT
,UPDATE
)了一些符合搜索條件P的數(shù)據(jù),此時前一個事務(wù)再次讀取符合條件P的記錄時就會獲取到之前沒有讀取過的記錄。這個現(xiàn)象叫做幻讀。
4. 回答一些可能存在的問題
現(xiàn)在是中場答疑時間。
一個事務(wù)讀到未提交事務(wù)修改的數(shù)據(jù)不行,讀到已提交事務(wù)修改的數(shù)據(jù)為什么還不行?
你是不是覺得一個事務(wù)讀取到其他事務(wù)最新提交的數(shù)據(jù)是一種正常現(xiàn)象?或者說在多數(shù)情況下這是我們期望的一種行為?沒錯,這種現(xiàn)象確實是正常的。不是說不行,而是針對我們討論的讀一致性問題上,這兩種現(xiàn)象都算是并發(fā)問題,因為談這個問題的時候我們已經(jīng)把語境固定死了,就是在同一個事務(wù)中的前后兩次SELECT
的操作結(jié)果不該和其他事務(wù)產(chǎn)生瓜葛,否則就是出現(xiàn)了讀一致性問題。
我只聽說過事務(wù)的一致性,沒聽說過讀一致性
事務(wù)在并發(fā)執(zhí)行時一共有下面3種情況:
- 讀-讀:并發(fā)事務(wù)相繼讀取相同記錄,由于讀取操作本身不會改變記錄的值,因此這種情況下自然不會有并發(fā)問題;
- 讀-寫/寫-讀:一個事務(wù)進(jìn)行讀取操作,另一個事務(wù)進(jìn)行寫(增刪改)操作;
- 寫-寫:并發(fā)事務(wù)相繼對相同記錄進(jìn)行寫(增刪改)操作。
不知道你有沒有注意到上一節(jié)的標(biāo)題是「事務(wù)并發(fā)執(zhí)行導(dǎo)致的讀問題」。并且臟讀、不可重復(fù)讀和幻讀都是在讀-寫/寫-讀的情況下出現(xiàn)的,那寫-寫情況怎么辦?
一切的并發(fā)問題都可以通過串行化解決,但是串行化效率太低了!
再優(yōu)化一下,一切并發(fā)問題都可以通過加鎖來解決,這種方案我們稱為基于鎖的并發(fā)控制(Lock Bases Concurrency Control, LBCC)!但是在讀多寫少的環(huán)境下,客戶端連讀取幾條記錄都需要排隊,效率還是太低了!
難不成數(shù)據(jù)庫有避免給讀操作加鎖就可以解決一致性問題的方法?沒錯,接下來我們要講的就是這個方法,所以我們才把一致性問題分為讀一致性和寫一致性,而寫一致性就得依賴數(shù)據(jù)庫的鎖機制了。
心急吃不了熱豆腐,這篇文章先給你講明白讀一致性問題。
不可重復(fù)讀和幻讀的最大區(qū)別是什么?
這個問題的答案在網(wǎng)上五花八門,要回答這個問題自然要找官方了。這個官方不是MySQL官方,而是美國國家標(biāo)準(zhǔn)協(xié)會(ANSI)。
我們上面談到的臟讀、不可重復(fù)讀和幻讀問題都是理論知識,并不涉及到具體的數(shù)據(jù)庫??紤]到所有數(shù)據(jù)庫在設(shè)計的過程中都可能遇到這些問題,ANSI就制定了一個SQL標(biāo)準(zhǔn),其中最著名的就是SQL92標(biāo)準(zhǔn),其中定義了「不可重復(fù)讀」和「幻讀」(當(dāng)然也定義了臟讀,但鑒于沒啥異議,我就沒截圖),我把其中的重點單詞給大家標(biāo)注了一下,希望大家能徹底搞懂兩者的區(qū)別。
我用中文翻譯一下就是:
不可重復(fù)讀:事務(wù)T1
讀取了一條記錄,事務(wù)T2
修改或者刪除了同一條記錄,并且提交。如果事務(wù)T1
試圖再次讀取同一條記錄的時候,會讀到被事務(wù)T2
修改的數(shù)據(jù)或者壓根讀不到。
幻讀:事務(wù)T1
首先讀取了符合某些搜索條件P的一些記錄。然后事務(wù)T2
執(zhí)行了某些SQL語句產(chǎn)生了符合搜索條件P的一條或多條記錄。如果事務(wù)T1
再次讀取符合條件P的記錄,將會得到不同于之前的數(shù)據(jù)集。
SQL標(biāo)準(zhǔn)對于不可重復(fù)讀已經(jīng)說得很清楚了,事務(wù)T2
要對T1
讀取的記錄進(jìn)行修改或者刪除操作,并且必須要提交事務(wù)。但是對于幻讀的定義就說得很模糊,尤其是文中使用了generate(生成/產(chǎn)生)
,再結(jié)合one or more rows
,我們可以認(rèn)為事務(wù)T2
執(zhí)行了INSERT
語句插入了之前沒有讀到的記錄,或者是執(zhí)行了更新記錄鍵值的UPDATE
語句生成了符合T1
之前的搜索條件的記錄,總之只要是事務(wù)T1
之前沒有讀到的數(shù)據(jù),都算是幻影數(shù)據(jù),至于事務(wù)T2
需不需要提交壓根兒沒提。
5. SQL標(biāo)準(zhǔn)與4種隔離級別
如果按照對一致性影響的嚴(yán)重程度,對上面提到的3種并發(fā)讀問題排個序的話,就是下圖這樣:
我們剛才也提到了,這3種并發(fā)讀問題都是理論知識,并不涉及到具體的數(shù)據(jù)庫。因此SQL標(biāo)準(zhǔn)再次發(fā)揮了作用,他們建議數(shù)據(jù)庫廠家按照他們的規(guī)范,提供給用戶4種隔離級別,讓用戶根據(jù)自己的業(yè)務(wù)需要權(quán)衡利弊,選擇合適的隔離級別,以此解決所有的并發(fā)讀問題(臟讀、不可重復(fù)讀、幻讀)或者對某些無關(guān)緊要的并發(fā)讀問題做出妥協(xié)。
SQL標(biāo)準(zhǔn)中定義的隔離級別有如下4種:
READ UNCOMMITTED
:未提交讀READ COMMITTED
:已提交讀REPEATABLE READ
:可重復(fù)讀SERIALIZABLE
:串行化
SQL標(biāo)準(zhǔn)中規(guī)定,針對不同的隔離級別,并發(fā)事務(wù)執(zhí)行過程中可以發(fā)生不同的并發(fā)讀問題。
其中綠色箭頭表示隔離級別由弱到強,紅色箭頭表示并發(fā)問題的嚴(yán)重程度由弱變強。翻譯一下上面的表格就是:
- 在
READ UNCOMMITTED
隔離級別下,臟讀、不可重復(fù)讀和幻讀都有可能發(fā)生。也就是這種隔離級別啥也沒干; - 在
READ COMMITTED
隔離級別下,不可能發(fā)生臟讀現(xiàn)象,但是不可重復(fù)讀和幻讀有可能發(fā)生; - 在
REPEATABLE READ
隔離級別下,可能發(fā)生幻讀現(xiàn)象,但是絕不可能發(fā)生臟讀和不可重復(fù)讀; - 在
SERIALIZABLE
隔離級別下,上述所有現(xiàn)象都不可能發(fā)生。
說完這些,有些人可能像當(dāng)時的我一樣,依舊是懵的。為什么要設(shè)置隔離級別?事務(wù)T1
讀到其他事務(wù)最新修改的數(shù)據(jù)難道不好嗎?為什么這些隔離級別的中文翻譯這么蹩腳,感覺好不通順啊。為什么單單給隔離性設(shè)置了級別?
5.1. 為什么要設(shè)置隔離級別?
說實話,我至今還沒遇到過需要我手動修改MySQL隔離級別的業(yè)務(wù),而且我也相信,短時間也不會出現(xiàn)這種場景。我相信大部分開發(fā)者也是一樣。因此,在沒有機會實戰(zhàn)的情況下,要能記住隔離級別的這個概念,必須從需求出發(fā),來理解為什么需要隔離級別。
我舉一個例子,假設(shè)你有一個賬單系統(tǒng),每個月底需要對你所有的客戶的借貸操作和賬戶余額進(jìn)行對賬。對此你寫了一個定時任務(wù),每個月初1號的00:00:00時刻開始啟動對賬業(yè)務(wù),由于是只對上個月的業(yè)務(wù)進(jìn)行對賬,所以該時刻之后所有的對該用戶賬戶的寫操作都不應(yīng)該對對賬事務(wù)的讀操作可見。
現(xiàn)在你知道并不是任何情況下都要讀取到最新修改的數(shù)據(jù)了吧。
5.2. 蹩腳的中文翻譯
至于中文蹩腳的問題,純屬是我個人揣測的了。因為直到現(xiàn)在我都覺得隔離級別的中文翻譯不順口,因此猜測可能讀這篇文章的其中一個你也會和我有同樣的問題呢。我的辦法就是直接用英文代替中文翻譯,純屬個人方法,不好使不要怪我。
5.3. 為什么單單給隔離性設(shè)置了級別?
終于聊到了為什么單單給隔離性設(shè)置了級別這個問題了。如果想想事務(wù)的4個特性,也就自然明白這個問題了。
原子性
簡單總結(jié)就是一個事務(wù)中的語句,要么全部執(zhí)行成功,要么全部執(zhí)行失敗,不允許存在中間狀態(tài)。所以對于原子性沒有級別可以設(shè)置,我們總不能提出至少有80%的SQL語句執(zhí)行成功這種無理的要求吧。
一致性
一致性是事務(wù)的最終目標(biāo)。簡而言之就是數(shù)據(jù)庫的數(shù)據(jù)操作之后的最終結(jié)果符合我們的預(yù)期,符合現(xiàn)實世界的規(guī)定。比如,陀螺賬戶里有100塊錢,招財分文無有,不管陀螺借給招財多少次,招財分成多少次還,他倆的賬戶總額必須是100,有借必有貸,借貸必相等,這就是一致性。呃。。。好像也沒找到可以商量商量打個折扣的點。
持久性
這個特性最簡單,就是要把事務(wù)的所有寫操作持久化到磁盤。我們自然也不可能提出至少80%的寫操作被持久化到磁盤這樣傻兮兮的要求吧。
隔離性
我們唯獨可以在這個隔離性上做點手腳。以新冠疫情為例。疫情緊張的時候,我們最常聽到的詞就是隔離,雖然都是隔離,卻有居家隔離、方艙隔離、酒店單間隔離之分。
再舉個例子,你和鄰居以墻相隔,這是一種很強的隔離性。但是某一天,你鑿壁偷了個光,你們兩家依然是有隔離性的,畢竟墻還在那立著呢,但是隔離性顯然沒有原來那么強了。說到這兒,不知道你理解了嗎?
6. MySQL支持的4種隔離級別
標(biāo)準(zhǔn)歸標(biāo)準(zhǔn),不同的數(shù)據(jù)庫廠商或者存儲引擎對標(biāo)準(zhǔn)的實現(xiàn)有一定的差異。比如Oracle數(shù)據(jù)庫只提供了READ COMMITTED
和SERIALIZABLE
兩種隔離級別。
說回MySQL。 InnoDB支持的4個隔離級別和SQL標(biāo)準(zhǔn)定義的完全一致,隔離級別越高,事務(wù)的并發(fā)程度就越低,但是出現(xiàn)并發(fā)問題的概率就越小。
上圖中還有非常重要的一點,就是InnoDB在REPEATABLE READ
隔離級別下,在很大程度上就解決了幻讀的問題,讓幻讀的發(fā)生成為一種小概率事件。在這一點上InnoDB不僅完成了SQL標(biāo)準(zhǔn),一定程度上也可以說是超越了標(biāo)準(zhǔn)。因此,REPEATABLE READ
也成了InnoDB默認(rèn)的隔離級別。
那什么時候幻讀還會發(fā)生呢?我舉個例子。我用兩個終端分別開啟兩個MySQL會話,每個會話中開啟了一個事務(wù),并且保證了每個會話的事務(wù)隔離級別都是REPEATABLE READ
。
# 事務(wù)T1首先開啟事務(wù) mysql> BEGIN; # 事務(wù)T1搜索id為1的記錄,結(jié)果集為空 mysql> SELECT * FROM user_innodb WHERE id = 1; Empty set (0.01 sec) # 事務(wù)T2插入一條id為1的記錄,并且提交事務(wù) # INSERT INTO user_innodb VALUES(1,'wanggangdan',0); # COMMIT; # 事務(wù)T1在重新搜索之前,修改一下事務(wù)T2剛插入的那條記錄 mysql> UPDATE user_innodb SET name = 'chanmufeng' WHERE id = 1; Query OK, 1 row affected (0.03 sec) # 事務(wù)T1再搜索id為1的記錄,發(fā)現(xiàn)多了一條記錄 mysql> SELECT * FROM user_innodb WHERE id = 1; +----+------------+--------+ | id | name | gender | +----+------------+--------+ | 1 | chanmufeng | 0 | +----+------------+--------+ 1 row in set (0.00 sec)
要說清楚這個問題就牽扯到MySQL鎖的知識了,這個以后再說,知道這么回事兒就行了。
回到我們的主線。我們現(xiàn)在想讓事務(wù)在自己的一畝三分地兒里隨便折騰,其他事務(wù)的增刪改操作我不想知道(或者我想知道,就放開一下隔離級別)。怎么辦?
或許你用過git
?我們用git
進(jìn)行開發(fā)任務(wù)的時候,通常情況下都會自己創(chuàng)建一個分支,在自己的分支上完成自己的任務(wù),這樣和其他開發(fā)者不會造成沖突。我們可以借鑒一下這個思路。
git
的每個分支都有一個分支id,那事務(wù)也該有自己的唯一標(biāo)識吧,這是自然的,下面稍微回顧一下行格式。
7. 行格式
7.1. 簡易版行格式
你存入MySQL的每一條記錄都會以某一種MySQL提供的行格式來進(jìn)行存儲,具體有哪些行格式我不打算說明,你也沒必要記住,他們之間的最大區(qū)別只是對磁盤占用率的優(yōu)化程度不同罷了。
我們把所有行格式的公有部分拿出來,總之,一條用戶數(shù)據(jù)可以用下面的圖來表示:
注:圖中標(biāo)識的字段順序和實際MySQL的字段存儲順序并不一致,這樣畫是為了能更清晰地說明問題。
roll_pointer
:是我們接下來聊的重點,這里先不管它;trx_id
:它就是事務(wù)id了,每條用戶記錄都有這個字段。千萬不要忘了一個至關(guān)重要的前提,我們用的存儲引擎是InnoDB;- 其他:就不用多說了吧。
7.2. 分配事務(wù)id的時機
對于讀寫事務(wù)而言,只有在它第一次對某個表進(jìn)行增刪改操作時,才會為這個事務(wù)分配一個事務(wù)id,否則不會分配。
更特殊地,如果一個讀寫事務(wù)中全是查詢語句,沒有增刪改的操作,這個事務(wù)也不會被分配事務(wù)id。
如果不分配事務(wù)id,事務(wù)id的值默認(rèn)為0
。
8. MVCC登場
8.1. 版本鏈
當(dāng)一個事務(wù)T1
讀到了一條記錄,我們當(dāng)然希望能禁止其他事務(wù)對該條記錄進(jìn)行修改和刪除的操作,直到T1
結(jié)束,但是這種滿足一己之私的行為在并發(fā)領(lǐng)域是要遭到唾罵的。這嚴(yán)重拖系統(tǒng)后腿啊。
于是InnoDB的設(shè)計者提出了一種和git
類似的想法,每對記錄做一次修改操作,都要記錄一條修改之前的日志,并且該日志還保存了當(dāng)前事務(wù)的id,和行格式類似,這條日志也有一個roll_pointer
節(jié)點。
實際InnoDB的這個功能和git沒有半毛錢關(guān)系,這里單純?yōu)榱祟惐取?/p>
當(dāng)對同一條記錄更新的次數(shù)多了,所有的這些日志會被roll_pointer
屬性連接成一個單鏈表,這個鏈表就是版本鏈,而版本鏈的頭節(jié)點就是當(dāng)前記錄的最新值。
注:這種日志的格式和普通記錄的格式其實并不相同,上圖中我們只關(guān)注兩者之間共同的部分。
上圖展示了一條記錄的版本鏈。該條記錄的最初始版本是由id為21
的事務(wù)進(jìn)行UPDATE
得到的(大家可以想一下,這里為什么不可能是INSERT
呢?)
后來,這條記錄分別被事務(wù)280
和事務(wù)300
各自連續(xù)UPDATE
了兩次。這里有個細(xì)節(jié),事務(wù)280
和事務(wù)300
并沒有交叉著更新這條記錄,這是為什么呢?也留給親愛的你思考吧。
InnoDB正是利用這個版本鏈來控制不同事務(wù)訪問相同記錄的行為,這種機制就是MySQL大名鼎鼎的MVCC
(Multi-Version Concurrency Control),多版本并發(fā)控制。
而上文中我們一直提及的日志,就是大名鼎鼎的undo
日志。
除了標(biāo)題,在正文中我盡量沒有提及
MVCC
術(shù)語,可把我憋壞了。因為對于沒有了解過這個概念的讀者而言,這個術(shù)語確實有點讓人害怕。不過看到這兒的話,是不是覺得也不過如此呢?
接下來就是看一下MySQL的各個隔離級別是怎么利用MVCC
的。
8.2. ReadView
READ UNCOMMITTED
隔離級別啥也不是,臟讀、不可重讀和幻讀問題一個都解決不了,所以干脆在這個隔離級別下直接讀取記錄的最新版本就得了。
而SERIALIZALE
隔離級別又矯枉過正,必須得用鎖機制才能實現(xiàn),所以就先按下不表了。
對于使用READ COMMITTED
和REPEATABLE READ
隔離級別的事務(wù)而言,決不允許發(fā)生臟讀現(xiàn)象(忘記了的話再回去看看表格),也就是說如果事務(wù)T2
已經(jīng)修改了記錄但是沒有提交,那T1
就不能直接讀取T2
修改之后的內(nèi)容。
現(xiàn)在的核心問題就是,怎么判斷版本鏈中的哪個版本是當(dāng)前事務(wù)可見的。
為此,InnoDB的設(shè)計者提出了ReadView
的概念,其中包含了4個比較重要的內(nèi)容:
m_ids
:生成ReadView
時,當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)id列表;min_trx_id
:生成ReadView
時,當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,也就是m_ids
中的最小值;max_trx_id
:生成ReadView
時,待分配給下一個事務(wù)的id號;creator_trx_id
:生成當(dāng)前ReadView
的事務(wù)的事務(wù)id。
有了ReadView
這個數(shù)據(jù)結(jié)構(gòu),事務(wù)判斷可見性的規(guī)則就是這樣的:
- 從版本鏈中的最新版本開始判斷
- 如果被訪問版本的
trx_id = creator_trx_id
,說明這個版本就是當(dāng)前事務(wù)修改的,允許訪問; - 如果被訪問版本的
trx_id < min_trx_id
(未提交事務(wù)的最小id),說明生成這個版本的事務(wù)在當(dāng)前ReadView
生成之前就已經(jīng)提交了,允許訪問; - 如果被訪問版本的
trx_id > max_trx_id
(待分配的事務(wù)id),說明生成這個版本的事務(wù)是在當(dāng)前ReadView生成之后建立的,不允許訪問; - 如果被訪問版本的
trx_id
在min_trx_id
和max_trx_id
之間,那就需要判斷trx_id
是否在m_ids
之中,如果在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問。否則,可以訪問; - 如果當(dāng)前版本不可見,就沿著版本鏈找到下一個版本,重復(fù)上面的1~4步。
READ COMMITTED
和REPEATABLE READ
隔離級別之間的不同之處就是生成ReadView
的時機不同。接下來具體看一下它們之間的區(qū)別。
8.2.1. READ COMMITTED
READ COMMITTED
是每次讀取數(shù)據(jù)之前都生成一個ReadView
。
我們來做個實驗,實驗之前先看一下我們的目標(biāo)記錄現(xiàn)在的值:
mysql> SELECT * FROM user_innodb WHERE id = 1; +----+-------------+--------+ | id | name | gender | +----+-------------+--------+ | 1 | wanggangdan | 1 | +----+-------------+--------+
假設(shè)系統(tǒng)中有兩個事務(wù)id分別為100
,200
的事務(wù)T1
、T2
在執(zhí)行:
# 事務(wù)T1(100)開始執(zhí)行 mysql> BEGIN; mysql> UPDATE user_innodb SET name = 'chanmufeng' WHERE id = 1; mysql> UPDATE user_innodb SET name = 'zhaosi' WHERE id = 1; # 注意,事務(wù)T1(100)并沒有提交
# 事務(wù)T2(200)開始執(zhí)行 mysql> BEGIN; # 做了其他表的一些增刪改操作 # 注意,事務(wù)T2(200)并沒有提交
此時,表user_innodb
中id為1的記錄的版本鏈的形式如下圖所示:
接下來我們在新的會話中將隔離級別設(shè)置為READ COMMITTED
,并開始事務(wù)T3
# 在新的會話中設(shè)置SESSION級別的隔離級別,這種設(shè)置方式對當(dāng)前會話的后續(xù)所有事務(wù)生效 mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; # 查看當(dāng)前會話默認(rèn)的隔離級別,發(fā)現(xiàn)是READ-COMMITTED,說明設(shè)置成功 mysql> SHOW VARIABLES LIKE 'transaction_isolation'; +-----------------------+----------------+ | Variable_name | Value | +-----------------------+----------------+ | transaction_isolation | READ-COMMITTED | +-----------------------+----------------+ # T3開啟事務(wù) mysql> BEGIN; # T3查詢id為1的記錄信息,發(fā)現(xiàn)是最原始的、事務(wù)T1修改之前的版本 mysql> SELECT * FROM user_innodb WHERE id = 1; +----+-------------+--------+ | id | name | gender | +----+-------------+--------+ | 1 | wanggangdan | 1 | +----+-------------+--------+
我們對照著上文說過的可見性判斷規(guī)則,來捋一遍整個流程:
T3
執(zhí)行SELECT
時會首先生成一個ReadView
數(shù)據(jù)結(jié)構(gòu),這個ReadView
的信息如下m_ids
列表的內(nèi)容是[100,200]
min_trx_id
為100
max_trx_id
為201
(這里我們假設(shè)待分配給下一個事務(wù)就是201
)creator_trx_id
為0
(因為事務(wù)T3
只是SELECT
而已,沒有做增刪改操作,所以事務(wù)id為0
)
- 從版本鏈中的最新版本開始判斷;
- 最新版本的
trx_id
是100
,在min_trx_id
和max_trx_id
之間,繼續(xù)判斷trx_id
是否在m_ids
之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本; - 當(dāng)前版本的
trx_id
是100
,不允許訪問,理由同上,繼續(xù)跳到下一個版本; - 當(dāng)前版本的
trx_id
是99
,小于min_trx_id
值100
,所以當(dāng)前版本對T3
可見,返回的數(shù)據(jù)就是name
為'wanggangdan'的這條記錄。
接著,實驗繼續(xù),我們把T1
提交一下:
# 事務(wù)T1提交 mysql> COMMIT;
然后在事務(wù)T2
中(目前還沒有提交)再次更新id為1的記錄
# 事務(wù)T2繼續(xù)執(zhí)行id為1的記錄的更新操作,但是依然不提交 mysql> UPDATE user_innodb SET name = 'wangwu' WHERE id = 1; mysql> UPDATE user_innodb SET name = 'wanger' WHERE id = 1;
現(xiàn)在,版本鏈就變成了這個樣子:
最后在事務(wù)T3
中重新執(zhí)行查詢,再來看一下結(jié)果集會是什么:
# 事務(wù)T3再次執(zhí)行查詢 mysql> SELECT * FROM user_innodb WHERE id = 1; +----+--------+--------+ | id | name | gender | +----+--------+--------+ | 1 | zhaosi | 1 | +----+--------+--------+
我們來捋一下這次的查詢的執(zhí)行過程:
- 因為
T3
的隔離級別是READ COMMITTED
,所以再次執(zhí)行查詢會重新生成一個ReadView
,此時這個ReadView
里邊的信息如下:m_ids
列表的內(nèi)容是[200]
,因為T1
已經(jīng)提交了min_trx_id
為200
max_trx_id
為201
(這里我們假設(shè)待分配給下一個事務(wù)就是201
)creator_trx_id
為0
(因為事務(wù)T3
只是SELECT
而已,沒有做增刪改操作,所以事務(wù)id為0
)- 從版本鏈中的最新版本開始判斷;
- 最新版本的
trx_id
是200
,在min_trx_id
和max_trx_id
之間,繼續(xù)判斷trx_id
是否在m_ids
之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本; - 當(dāng)前版本的
trx_id
是200
,不允許訪問,理由同上,繼續(xù)跳到下一個版本; - 當(dāng)前版本的
trx_id
是100
,小于min_trx_id
值200
,所以當(dāng)前版本對T3
可見,返回的數(shù)據(jù)就是name
為'zhaosi'的這條記錄。
重點就是:READ COMMITTED在每次SELECT的時候都重新生成一個ReadView。
注意:在做實驗的時候如果長時間未操作終端,可能導(dǎo)致和MySQL服務(wù)器的連接自動斷開,連接一旦斷開,事務(wù)會自動進(jìn)行提交。做實驗的小伙伴需要注意一下。
8.2.2. REPEATABLE READ
學(xué)會了READ COMMITTED
,REPEATABLE READ
也是同樣的道理了,唯一的區(qū)別是:
只會在第一次執(zhí)行SELECT
的時候生成一個ReadView
,之后不管SELECT
多少次,都是用最開始生成的ReadView
中的變量進(jìn)行判斷。
還是拿上面的事務(wù)id為100
和200
的事務(wù)為例,在實驗之前,先將數(shù)據(jù)重置到最初的狀態(tài)。
mysql> UPDATE user_innodb SET name = 'wanggangdan' WHERE id = 1;
事務(wù)T1
先執(zhí)行:
# 事務(wù)T1(100)開始執(zhí)行 mysql> BEGIN; mysql> UPDATE user_innodb SET name = 'chanmufeng' WHERE id = 1; mysql> UPDATE user_innodb SET name = 'zhaosi' WHERE id = 1; # 注意,事務(wù)T1(100)并沒有提交
# 事務(wù)T2(200)開始執(zhí)行 mysql> BEGIN; # 做了其他表的一些增刪改操作 # 注意,事務(wù)T2(200)并沒有提交
此時,表user_innodb
中id為1的記錄的版本鏈的形式如下圖所示:
接下來我們在新的會話中將隔離級別設(shè)置為REPEATABLE READ
,并開始事務(wù)T3
# 在新的會話中設(shè)置SESSION級別的隔離級別,這種設(shè)置方式對當(dāng)前會話的后續(xù)所有事務(wù)生效 mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; # 查看當(dāng)前會話默認(rèn)的隔離級別,發(fā)現(xiàn)是READ-COMMITTED,說明設(shè)置成功 mysql> SHOW VARIABLES LIKE 'transaction_isolation'; +-----------------------+-----------------+ | Variable_name | Value | +-----------------------+-----------------+ | transaction_isolation | REPEATABLE-READ | +-----------------------+-----------------+ # T3開啟事務(wù) mysql> BEGIN; # T3查詢id為1的記錄信息,發(fā)現(xiàn)是最原始的、事務(wù)T1修改之前的版本 mysql> SELECT * FROM user_innodb WHERE id = 1; +----+-------------+--------+ | id | name | gender | +----+-------------+--------+ | 1 | wanggangdan | 1 | +----+-------------+--------+
現(xiàn)在捋這個流程你應(yīng)該已經(jīng)熟悉很多了:
T3
初次執(zhí)行SELECT
時會生成一個ReadView
數(shù)據(jù)結(jié)構(gòu),這個ReadView
的信息如下m_ids
列表的內(nèi)容是[100,200]
min_trx_id
為100
max_trx_id
為201
(這里我們假設(shè)待分配給下一個事務(wù)就是201
)creator_trx_id
為0
(因為事務(wù)T3
只是SELECT
而已,沒有做增刪改操作,所以事務(wù)id為0
)
- 從版本鏈中的最新版本開始判斷;
- 最新版本的
trx_id
是100
,在min_trx_id
和max_trx_id
之間,繼續(xù)判斷trx_id
是否在m_ids
之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本; - 當(dāng)前版本的
trx_id
是100
,不允許訪問,理由同上,繼續(xù)跳到下一個版本; - 當(dāng)前版本的
trx_id
是99
,小于min_trx_id
值100
,所以當(dāng)前版本對T3
可見,返回的數(shù)據(jù)就是name
為'wanggangdan'的這條記錄。
接著,實驗繼續(xù),我們把T1
提交一下:
# 事務(wù)T1提交 mysql> COMMIT;
然后在事務(wù)T2
中(目前還沒有提交)再次更新id為1的記錄
# 事務(wù)T2繼續(xù)執(zhí)行id為1的記錄的更新操作,但是依然不提交 mysql> UPDATE user_innodb SET name = 'wangwu' WHERE id = 1; mysql> UPDATE user_innodb SET name = 'wanger' WHERE id = 1;
現(xiàn)在,版本鏈就變成了這個樣子:
最后在事務(wù)T3
中重新執(zhí)行查詢,再來看一下結(jié)果集會是什么:
# 事務(wù)T3再次執(zhí)行查詢 mysql> SELECT * FROM user_innodb WHERE id = 1; +----+-------------+--------+ | id | name | gender | +----+-------------+--------+ | 1 | wanggangdan | 1 | +----+-------------+--------+
我們來捋一下這次的查詢的執(zhí)行過程:
- 因為
T3
的隔離級別是REPEATABLE READ
,所以還是沿用一開始生成的那個ReadView
,再抄一遍:m_ids
列表的內(nèi)容是[100,200]
min_trx_id
為100
max_trx_id
為201
creator_trx_id
為0
- 從版本鏈中的最新版本開始判斷;
- 最新版本的
trx_id
是200
,在min_trx_id
和max_trx_id
之間,繼續(xù)判斷trx_id
是否在m_ids
之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本; - 當(dāng)前版本的
trx_id
是200
,不允許訪問,理由同上,繼續(xù)跳到下一個版本; - 當(dāng)前版本的
trx_id
是100
,在min_trx_id
和max_trx_id
之間,繼續(xù)判斷trx_id
是否在m_ids
之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView
時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本; - 當(dāng)前版本的
trx_id
是100
,不允許訪問,理由同上,繼續(xù)跳到下一個版本; - 當(dāng)前版本的
trx_id
是99
,小于min_trx_id
值100
,所以當(dāng)前版本對T3
可見,返回的數(shù)據(jù)就是name
為'wanggangdan'的這條記錄。
也就是說,READ COMMITTED
隔離級別下,T3
前后兩次SELECT
得到的結(jié)果完全一致,跟其他事務(wù)提交不提交沒有關(guān)系,即使事務(wù)T2
后來也提交了,也不影響T3
的搜索結(jié)果。怎么樣,是不是一致性的程度比READ COMMITTED
更強了呢?
到此這篇關(guān)于MYSQL事務(wù)的隔離級別與MVCC的文章就介紹到這了,更多相關(guān)SQL事務(wù)隔離與MVCC內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mysql 5.6.37 winx64安裝雙版本mysql筆記記錄
機器上現(xiàn)在已經(jīng)存在5.0版本MySQL的情況下裝一個最新版的mysql,下文通過實例代碼給大家介紹Mysql 5.6.37 winx64安裝雙版本mysql筆記記錄,感興趣的朋友一起看看吧2017-07-07SpringBoot中mysql的驅(qū)動依賴問題小結(jié)
這篇文章主要介紹了SpringBoot中mysql的驅(qū)動依賴問題,本文通過圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-05-059種 MySQL數(shù)據(jù)庫優(yōu)化的技巧
這篇文章小編主要給大家介紹的是 MySQL數(shù)據(jù)庫優(yōu)化的正確姿勢,九種方法呢?。。⌒枰男』锇橼s快收藏起來吧2021-09-09Mysql如何通過binlog日志恢復(fù)數(shù)據(jù)詳解
binlog日志用于記錄所有更新了數(shù)據(jù)或者已經(jīng)潛在更新了數(shù)據(jù)的所有語句,下面這篇文章主要給大家介紹了關(guān)于Mysql如何通過binlog日志恢復(fù)數(shù)據(jù)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02