手把手帶你探索 MySQL 事務的隔離

Remember發表於2019-10-26

手把手帶你探索MySQL事務的隔離

開篇宣告,這篇文章是最近學習極客專欄後所寫的,文中部分內容和圖片來自於極客專欄。學習的最好方式就是把自己學的東西通過文字表現出來,並傳遞給他人。本篇文章會從理論到實踐一步步驗證。本文預設InnoDB引擎。

:pencil2:事務的概念

日常開發的時候,對事務再熟悉不過了吧。典型的一個事務場景就是轉賬功能吧。A賬戶有100元,B賬戶有0元。當前A給B轉賬100元。轉賬過程中涉及到查餘額,做加減賬戶餘額,這些操作必須都是一體的。餘額大於等於轉賬金額才能進行轉賬,從A賬戶減100,往B賬戶加100,如果這個時候,給B加100成功了,A賬戶還沒減,我完全可以利用時間差再轉一次錢,這時候多出來的100不就亂了嘛。所以說在上述整個操作中,要麼全做(A-100, B+100),要麼什麼都別動,不能只動一邊,保證資料原子性。就像你被帶綠帽的同時總有另一個人給你帶綠帽,是一個道理,話糙理不糙。

簡單的說,為了保證資料的一致性和正確性,資料庫必須保證事務具有四個性質ACID(原子性,一致性,隔離性,持續性)。接下來會使用例子來理解隔離性。隔離性又是什麼呢?還是上個例子,如果我在上面操作的時候,有其他的操作插入進來修改A或者B金額,那麼就會導致資料錯亂,(就像emmm........,算了不開車了)所以事務之間要進行隔離。

:pencil2:隔離性與隔離級別

我們應該知道,事務之間的隔離,隔離的越嚴實,相應的效率也會越低,所以在操作的過程中,需要針對當前的環境,尋找一個平衡點。標準的事務隔離包括讀未提交,讀已提交,可重複讀以及可序列性。

  • 讀未提交: 一個事務做的操作還未提交,它做的變更就能被其他事務看到。

  • 讀已提交: 一個事務做的操作要等到他提交了事務之後才能被其他事務看到

  • 可重複讀: 一個事務在執行過程中看到的資料,總是和它在啟動的時候看到的資料是一樣的。當然它自己未提交的事務對其他事務來說也是不可見的。

  • 可序列性:對於同一行資料,寫會加“寫”鎖,讀會加"讀"鎖,當出現讀寫衝突的時候,後一個事務必須等待前一個事務提交事務,才能進行。

下面來實踐一個例子說明以上的內容。我們先建立一個表,並往其插入一條資料。

手把手帶你探索 MySQL 事務的隔離

MySQL的預設隔離級別是可重複讀。可以使用命令檢視一下。確實是可重複讀。通過修改隔離級別來驗證以上的內容。

手把手帶你探索 MySQL 事務的隔離

我們先修改成讀未提交,跑命令,然後檢視是否修改成功。

手把手帶你探索 MySQL 事務的隔離

我們同時開啟兩個視窗,代表著兩個事務,來驗證隔離級別為讀未提交的情況下,其他事務是否可以檢視到未提交事務的更改資料。

手把手帶你探索 MySQL 事務的隔離

視窗1在修改完值後並未提交事務,但是此時在視窗2事務中查詢已經可以看到視窗1修改的值。這裡開啟事務我使用了

start transaction with consistent snapshot

因為在MySQL中,begin/start transaction 並不是真正開啟事務,而是在執行InnoDB的第一句語句的時候,事務才真正的啟動。如果你想馬上開啟事務,那麼使用上面的語句。

接下來我們把隔離修改成讀已提交。把c=2重新修改成c=1,但是修改過程中不提交事務,看另一個事務是否對修改可見。

手把手帶你探索 MySQL 事務的隔離

檢視結果,可以看到當在讀已提交的隔離下,另一個事務看不到未提交事務的修改。

手把手帶你探索 MySQL 事務的隔離

我們可以接著看,把修改的事務提交之後,再進行查詢。可以很清楚的看到,在事務commit之後,另一個事務再查詢能看到對應修改的值。\

手把手帶你探索 MySQL 事務的隔離

接下來我們主要再看一下可序列性。首先我們修改對應的Mysql的隔離級別,然後開始執行。同時開啟兩個事務,查詢的時候沒有問題,當其中一個事務準備更新資料的時候,被鎖住了,游標停止不動。

手把手帶你探索 MySQL 事務的隔離

此時需要commit其中一個事務釋放鎖,另一個事務才可以執行操作。但是對於此時的改動,其他事務並不能看見,因為修改的事務並未提交。但是對於自己來說,我修改的東西當然可見(涉及到之後的知識),等到修改事務commit提交的時候,那麼再次查詢就可以看見修改的操作了。

手把手帶你探索 MySQL 事務的隔離

手把手帶你探索 MySQL 事務的隔離

理解了事務隔離的級別,那具體是咋麼實現的呢。在實現上,資料庫會生成一個檢視,訪問的時候以檢視的邏輯結果為準。在可重複讀的情況下,這個檢視是在事務開啟的時候就啟動的。整個事務存在期間使用的都是這個檢視。讀提交的情況下,檢視是在每個sql語句執行的時候建立的。如果是讀未提交隔離下,直接返回記錄的最新值,並不會建立檢視。至於序列性則是通過加鎖的方式來實現併發控制。

:pencil2:事務到底隔離還是不隔離

上面說到,在可重複讀的隔離下,事務在啟動的時候建立一個檢視,之後即使其他事務更改了資料,對當前這個執行中的事務來說,看到的檢視依然是啟動時的檢視,似乎不受外界影響。

但是上面有一個場景,在更新行資料的時候,如果剛好有另一個事務擁有這個行鎖,那麼當前事務更新行的時候將會被鎖住,等到另一個事務釋放了鎖,當前事務獲取了鎖之後,此時它查詢得到的值還是事務啟動時的值嗎?下面的實踐內容將圍繞這個話題展開。

首先這是表的結構和對應當前的兩條記錄。

手把手帶你探索 MySQL 事務的隔離

接下來我們有事務A.B.C的執行流程.

手把手帶你探索 MySQL 事務的隔離

還有一點就是MySQL有一個引數設定值autocommit,預設是1表示的是事務自動提交,每一個查詢都是一個單獨的事務自動提交,就像圖中事務C,update就是一個單獨的事務,更新完自己提交。當然你可以使用顯式的begin/commit。

讓我們把目光對準上面的圖,事務B的查詢結果K是3,事務A的查詢結果K是1,你是不是想罵我?你先別急著罵,讓我們來看下結果。開啟三個控制檯。

手把手帶你探索 MySQL 事務的隔離

好吧我沒瞎說吧,現在你可以開始罵我了,你不是說在可重複讀隔離的情況下,當前事務執行過程中看到的檢視始終是啟動時的檢視嘛。

在MySQL中有兩個檢視概念:

  • 一個是view。也就是建立檢視,語句是 create view xxx() as.....,它並不是一個真實的表,它的內容是由儲存在資料庫中進行查詢操作的SQL語句定義的。

  • 另一個就是InnoDB在實現MVCC是用到的一致性讀檢視(consistent read view)。用於支援讀已提交RC(Read Committed) 和可重複讀RR(Repeatable Read)的隔離級別實現。

InnoDB每一個事物都存在一個事務唯一ID,叫做transaction id,它是一個事務在啟動的時候InnoDB向系統申請的,嚴格按照遞增的形式生成。

每一行資料都有對應多個版本,每次通過事務更新的資料都會生成新的版本,然後把transaction id 賦值給這個版本事務ID,記為row trx_id,同時,為了之後可以恢復資料,我們需要保留舊的資料版本。也就是說在當前最新的版本中,我們可以隨時獲取舊的資料。下圖對應修改一行資料的版本圖。

手把手帶你探索 MySQL 事務的隔離

                                      注:圖片來源極客時間

從上圖可以知道,最新版本是V4,且是由事務 transaction id =25更新的,所以對應此行資料的資料版本row trx_id =25。

按照可重複讀的定義。一個事務啟動的時候,可以看到所有已經提交的事務,但是接下來的事務對它來說是不可見的。所以對於一個事務啟動的時候,如果一個事務在我啟動時刻之前生成的,我就認,如果在我啟動之後生成的,我就不認,我必須要找到它的上一個版本。有點渣男的嫌疑。如果上一個版本還不可見,那就繼續往前面找,當然在這個過程中,自己更新的東西得認。

在實現上,InnoDB為每一個事務建立了一個陣列。事務中ID最小的稱為低水位,當前系統中已經建立的事務ID最大值加1就是高水位。這個陣列和水點陣圖,就組成了當前事務的一致性圖。資料版本可見性,就是基於資料版本陣列和水位檢視的比較而得到的結果。

這個檢視陣列把所有的row trx_id分為以下幾種情況

手把手帶你探索 MySQL 事務的隔離

                                        注:圖片來源極客時間

  1. 如果當前事務啟動的時候,一個資料版本(row trx_id)落在綠色部分,表示這個事務是已提交的版本或者是自己生成的,可見。

  2. 如果落在紅色部分,說明這個版本是由將來啟動的事務生成的,肯定不可見。

  3. 如果落在黃色,又分為兩種情況。如果 row trx_id 存在陣列中,說明, 此時版本資料還未提交事務,不可見,如果不在,說明已經提交事務了,可見。   

接下來可以分析為什麼上面A查詢的k=1,B查詢的k=3了。

手把手帶你探索 MySQL 事務的隔離

檢視上圖,我們假設當前有一個活躍的事務99,目前我們更新的這一行資料的資料版本row trx_id=90,此時系統存在4個事務,那麼對於A的檢視陣列就是[99,100],B的檢視陣列就是[99,100,101],C的活躍陣列就是[99,100,101,102]。當前版本101。

從圖中知道,事務A先啟動,接著事務B,最後事務C,但是第一個有效更新的是事務C,設定k為2(k=1+1),然後是事務B 把k設定為3(k=2+1),接著到A查詢了,他的檢視陣列是[99,100],此時資料版本(1,3)也就是row trx_id=101 ,一對比,發現這貨在高水位。不可見,再往回追,(1,2)資料版本row trx_id=102,我去還是在高水位,不可見,最後追到(1,1)資料版本row trx_id=90,比水位低,可見,一看上面的值K=1,所以查詢結果就等於1。

這樣一個流程走下來,雖然中間修改了資料,資料版本也發生了變化,但是對於事務A來說,對他都是不可見的,所以看到的結果還是之前的資料。

到這裡還有一個疑問,也就是開頭的,事務B是在事務C之前開啟事務的啊,對於事務B來說,事務C的操作對他來說是不可見的啊,事務B為什麼獲取的值是2,然後再2的基礎上更新了,它啟動事務的時候k可不等於2。

是的道理是沒錯,前提是如果事務B在更新之前先查詢一遍資料,那麼之後在它更新完資料以後,再次查詢得到的值將會是1,而不是3。這裡運用到一條規則,更新資料的時候都是先讀後寫的。而這個讀,只能讀當前的值,叫做"當前讀"。對於B來說,在它更新之前,並沒有執行讀操作,所以在更新的時候,不能再在歷史版本上直接更新了,否則C的更新將會丟失。所以B在更新的時候當前讀(1,2),更新之後(1,3),當前的版本也就是 row trx_id=101了。等到他查詢的時候,一看當前版本號101就是自己,所以查詢的時候就等於3。

下面可以試驗一下當B在更新之前查詢一遍資料,然後更新資料再查詢,符合上面所說的,得到的值就是1。此時運用的就是當前讀。

手把手帶你探索 MySQL 事務的隔離

最後如果改動一下C事務中的執行,結果又是什麼?\

手把手帶你探索 MySQL 事務的隔離

這時候的事務C不再是自動提交事務,也是顯式提交。事務C'更新語句,先獲取寫鎖,但是C'事務還未提交,此時事務B是當前讀,必須讀取當前最新版本,而且必須加鎖,等到C提交了,事務B才得以繼續進行。那麼B查詢的結果不會變依然是之前的3,而事務C的事務已經提交了,在讀隔離的情況下,是在建立新檢視之前,所以A的結果k=2。

可重複讀的核心就是一致性讀。而事務更新資料的時候,只能用當前讀,如果當前要讀的記錄的行數被其他事務佔用的時候,就需要等待。

而讀提交的邏輯和可重複讀的邏輯類似,最主要的區別在於:

  • 在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視;

  • 在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的檢視

還是把這篇文章寫完了,極客專欄的質量真的很高,如果有理解有誤的地方,請慷慨指正。第一發布地址:https://mp.weixin.qq.com/s?__biz=MzU3Mzc5N...

吳親庫裡

相關文章