亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

MYSQL事務(wù)的隔離級別與MVCC

 更新時間:2022年05月25日 10:36:59   作者:??蟬沐風(fēng)????  
這篇文章主要介紹了MYSQL事務(wù)的隔離級別與MVCC,文章首先通過事務(wù)的相關(guān)內(nèi)容展開主題主要介紹,具有一定的參考價值,需要的小伙伴可以參一下

前言

提到數(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ù)功能的存儲引擎只有InnoDBNDB,鑒于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ù)并不沖突,如果不顯式使用BEGINSTART TRANSACTION開啟一個事務(wù),那么InnoDB會在每一條增刪改語句執(zhí)行之后提交事務(wù)。

如果我們把autocommit設(shè)為OFF,除非我們手動使用BEGINSTART TRANSACTION開啟一個事務(wù),否則InnoDB絕不會自動開啟事務(wù);同樣,除非我們使用COMMITROLLBACK提交或回滾事務(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ù)T1T2并發(fā)執(zhí)行,都要訪問user_innodb表中id1的數(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ù)讀

同樣是T1T2兩個事務(wù),T1通過id=1查詢到了一條數(shù)據(jù),然后T2緊接著UPDATEDELETE也可以)了該條記錄,不同的是,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. 幻讀

還是T1T2這倆貨,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 ControlLBCC)!但是在讀多寫少的環(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 COMMITTEDSERIALIZABLE兩種隔離級別。

說回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 COMMITTEDREPEATABLE 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_idmin_trx_idmax_trx_id之間,那就需要判斷trx_id是否在m_ids之中,如果在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問。否則,可以訪問;
  • 如果當(dāng)前版本不可見,就沿著版本鏈找到下一個版本,重復(fù)上面的1~4步。

READ COMMITTEDREPEATABLE 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_id100
    • max_trx_id201(這里我們假設(shè)待分配給下一個事務(wù)就是201
    • creator_trx_id0(因為事務(wù)T3只是SELECT而已,沒有做增刪改操作,所以事務(wù)id為0
  • 從版本鏈中的最新版本開始判斷;
  • 最新版本的trx_id100,在min_trx_idmax_trx_id之間,繼續(xù)判斷trx_id是否在m_ids之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本;
  • 當(dāng)前版本的trx_id100,不允許訪問,理由同上,繼續(xù)跳到下一個版本;
  • 當(dāng)前版本的trx_id99,小于min_trx_id100,所以當(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_id200
    • max_trx_id201(這里我們假設(shè)待分配給下一個事務(wù)就是201
    • creator_trx_id0(因為事務(wù)T3只是SELECT而已,沒有做增刪改操作,所以事務(wù)id為0
    • 從版本鏈中的最新版本開始判斷;
  • 最新版本的trx_id200,在min_trx_idmax_trx_id之間,繼續(xù)判斷trx_id是否在m_ids之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本;
  • 當(dāng)前版本的trx_id200,不允許訪問,理由同上,繼續(xù)跳到下一個版本;
  • 當(dāng)前版本的trx_id100,小于min_trx_id200,所以當(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 COMMITTEDREPEATABLE READ也是同樣的道理了,唯一的區(qū)別是:

只會在第一次執(zhí)行SELECT的時候生成一個ReadView,之后不管SELECT多少次,都是用最開始生成的ReadView中的變量進(jìn)行判斷。

還是拿上面的事務(wù)id為100200的事務(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_id100
    • max_trx_id201(這里我們假設(shè)待分配給下一個事務(wù)就是201
    • creator_trx_id0(因為事務(wù)T3只是SELECT而已,沒有做增刪改操作,所以事務(wù)id為0
  • 從版本鏈中的最新版本開始判斷;
  • 最新版本的trx_id100,在min_trx_idmax_trx_id之間,繼續(xù)判斷trx_id是否在m_ids之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本;
  • 當(dāng)前版本的trx_id100,不允許訪問,理由同上,繼續(xù)跳到下一個版本;
  • 當(dāng)前版本的trx_id99,小于min_trx_id100,所以當(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_id100
    • max_trx_id201
    • creator_trx_id0
    • 從版本鏈中的最新版本開始判斷;
  • 最新版本的trx_id200,在min_trx_idmax_trx_id之間,繼續(xù)判斷trx_id是否在m_ids之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本;
  • 當(dāng)前版本的trx_id200,不允許訪問,理由同上,繼續(xù)跳到下一個版本;
  • 當(dāng)前版本的trx_id100,在min_trx_idmax_trx_id之間,繼續(xù)判斷trx_id是否在m_ids之中,發(fā)現(xiàn)在,說明生成當(dāng)前ReadView時,生成該版本的事務(wù)還是活躍的,因此不允許訪問,根據(jù)鏈表找到下一個版本;
  • 當(dāng)前版本的trx_id100,不允許訪問,理由同上,繼續(xù)跳到下一個版本;
  • 當(dāng)前版本的trx_id99,小于min_trx_id100,所以當(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)文章

  • CentOS7.4手動安裝MySQL5.7的方法

    CentOS7.4手動安裝MySQL5.7的方法

    這篇文章主要介紹了CentOS7.4手動安裝MySQL5.7的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • Mysql 5.6.37 winx64安裝雙版本mysql筆記記錄

    Mysql 5.6.37 winx64安裝雙版本mysql筆記記錄

    機器上現(xiàn)在已經(jīng)存在5.0版本MySQL的情況下裝一個最新版的mysql,下文通過實例代碼給大家介紹Mysql 5.6.37 winx64安裝雙版本mysql筆記記錄,感興趣的朋友一起看看吧
    2017-07-07
  • mysql 主從服務(wù)器的簡單配置

    mysql 主從服務(wù)器的簡單配置

    首先呢,需要有兩個mysql服務(wù)器。如果做測試的話可以在同一臺機器上裝兩個mysql服務(wù)程序,注意要兩個運行程序的端口不能一樣。我用的是一個是默認(rèn)的3306,從服務(wù)器用的是3307端口。
    2009-05-05
  • SpringBoot中mysql的驅(qū)動依賴問題小結(jié)

    SpringBoot中mysql的驅(qū)動依賴問題小結(jié)

    這篇文章主要介紹了SpringBoot中mysql的驅(qū)動依賴問題,本文通過圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2023-05-05
  • MySQL分庫分表的幾種方式

    MySQL分庫分表的幾種方式

    這篇文章主要介紹了MySQL分庫分表的幾種方式,分庫分表方案是對關(guān)系型數(shù)據(jù)庫數(shù)據(jù)存儲和訪問機制的一種補充,下文更多相關(guān)介紹需要的小伙伴可以參考一下
    2022-04-04
  • 9種 MySQL數(shù)據(jù)庫優(yōu)化的技巧

    9種 MySQL數(shù)據(jù)庫優(yōu)化的技巧

    這篇文章小編主要給大家介紹的是 MySQL數(shù)據(jù)庫優(yōu)化的正確姿勢,九種方法呢?。。⌒枰男』锇橼s快收藏起來吧
    2021-09-09
  • Mysql?InnoDB中B+樹索引使用注意事項

    Mysql?InnoDB中B+樹索引使用注意事項

    這篇文章主要為大家介紹了Mysql?InnoDB中B+樹索引的注意事項,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-05-05
  • Mysql如何通過binlog日志恢復(fù)數(shù)據(jù)詳解

    Mysql如何通過binlog日志恢復(fù)數(shù)據(jù)詳解

    binlog日志用于記錄所有更新了數(shù)據(jù)或者已經(jīng)潛在更新了數(shù)據(jù)的所有語句,下面這篇文章主要給大家介紹了關(guān)于Mysql如何通過binlog日志恢復(fù)數(shù)據(jù)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2022-02-02
  • 修改MYSQL密碼的幾種常用方法總結(jié)

    修改MYSQL密碼的幾種常用方法總結(jié)

    以下是對修改MYSQL密碼的幾種常用方法進(jìn)行了總結(jié)介紹,需要的朋友可以過來參考下
    2013-08-08
  • Mysql忘記密碼的幾種解決方案

    Mysql忘記密碼的幾種解決方案

    今天小編就為大家分享一篇關(guān)于Mysql忘記密碼的幾種解決方案,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-03-03

最新評論