我勸!這位年輕人不講MVCC,耗子尾汁!

賜我白日夢發表於2020-12-03



Hi,大家好!我是白日夢。

今天我要跟你分享的話題是:“MySQL是如何根據undo log 鏈條實現read view機制的?談談看”



一、事務的隔離級別與MVCC?

MySQL單程式多執行緒的資料庫軟體,在事務的併發操作中可能會出現髒讀,不可重複讀,幻讀。

MySQL支援的四種事務隔離級別如下:

  • Read uncommited

    簡單來說就是:事務A可以讀到事務B未commit的資料。這種情況也被叫做髒讀。

  • Read commited

    簡單來說就是:事務A可以讀到事務B已經commit的資料。

  • Serializable

    在該級別下,寫會加寫鎖、讀會加讀鎖,除了讀讀不互斥,其他組合都互斥,因此可以保證事務序列化順序執行,可以避免髒讀、不可重複讀與幻讀。

  • Repeatable read

    如下圖:可重複讀要求事務A兩次 select 查詢出來的結果是一樣的,即使中間事務B將id=1的行給修改了,也要保證事務A再讀取時,讀到的結果也得和第一次讀到的結果相同。

但是可重複讀存在幻讀讀問題,比如事務A開啟後按某個範圍X讀取一次(事務未提交),這時其他事務在該範圍X內插入了新的資料,事務A再讀時就會將新插入的資料讀取出來,當然在MySQL的RR隔離級別下不會再出現這種幻行的問題。

問題的解決得益於:MVCC多版本併發控制的快照讀和next-key lock 當前讀。



二、Repeatable Read是如何實現的

以RR隔離級別為例:

你可以像下面這樣看一下你的MySQL預設使用的什麼隔離級別:

MVCC多版本併發控制也被稱為快照讀,在RR的隔離級別下,當事務開啟時會建立一個檢視(Read View),其實這個檢視就是所謂的快照。在整個事務存在的期間,一直會使用這個檢視。

下面看一個九個步驟的小實驗:

上圖中的右部分的會話中begin之後,就會建立讀檢視,所以它的多次select使用的是同一個檢視,所以結果都是一樣的。即使資料中途被左邊的事務更改了,它也沒有受到影響。

再結合檢視去理解這個過程。

當你執行begin開啟事務之後,MySQL會拍下像下圖這樣的快照:

上圖中的trx_ids中記錄著MySQL中活躍的且未提交的事務。

假設有事務A、事務B擦不多在同一時刻開啟,那這兩個事務會分別得到如下的檢視。

在RR的隔離級別下,事務一開啟就會得到上圖那樣的ReadView,並且只要事務不提交這個ReadView就一直有效。

就上圖來說:

在事務A的檢視中,它的事務ID=61,此時活躍的事務集合是[61、62],活躍的事務ID中最小的事務id是它本身。下一個事務id應該是63。

在事務B的檢視中,它的事務ID=621,此時活躍的事務集合是[61、62],活躍的事務ID中最小的事務id是61。下一個事務id應該是63。

先讓事務A嘗試去讀取name列的資料。

它會發現的這行資料的Data_TRX_ID=60,通過和trx_ids對比發現這個事務ID不在活躍的事務id集合trx_ids中,並且小於它本身的60。說明:在事務A開啟之前,事務ID=60的事務早就提交過了。所以事務A能直接這行資料name = tom。

然後事務B通過update語句嘗試去修改這行資料,想將name 改成 jetty。這時MySQL會記錄相應的undo log,並以連結串列的方式串聯起來,於是我們會得到下圖:

你可以看到上圖中,由於事務B將name改成jerry,導致多出一條undo log。這條undo對應的事務ID=事務B的事務ID = 62。並且通過一個指標執向它的上一個undo log記錄。

這時如果事務A重新去讀,首先它會讀取到的記錄是name = jerry,但是它也會發現該記錄的trx_id = 62 , 比自己的61還大,並且比下一個事務ID63小。說明:它讀到記錄其實是和自己同時開啟的事務修改後的產物,這時他就會沿著undo log鏈條往前找,直到找到第一個trx_id等於或者小於自己事務ID的記錄為止。所以事務A再一次讀取到trx_id = 60的記錄。

這也就是所謂的快照讀機制。

另外需要注意的是:就上例來說,在RR的隔離級別下,確實能保證事務A每次讀取出來的結果都是一樣的,而且在事務B將其修改後,事務A依然能讀取出name = tom。但是這時name=tom真的只是個快照,本質上它已經可以算是不存在是資料了。



本文是MySQL專題第15篇,全文近100篇(公眾號首發)

本文是第15篇,全文近100篇,點選檢視目錄




三、Read Commited是如何實現的:

在RR隔離級別下,當事務一開始檢視就會被建立出來,並且一直到該事務提交該檢視都有效。

在Read Commited隔離級別,每次select 都會建立一個新的檢視。

還是使用這個例子:假設事務A和事務B併發開啟,並且各自得到了圖中的ReadView。然後很快,事務B就將資料name = tom改成了name = jerry(未提交)。那這時事務A去select會檢索出什麼結果呢?

事務A檢索過程:事務A首先會沿著undo log鏈條從頭開始找,於是它首先找到name = jerry的列。但是它也發現該列的trx_id = 62 不但比自己的事務ID60大,而且還在trx_ids這個活躍事務列表中,說明name = jerry是被和自己差不多同時開啟的其他事務更改的。它自然也就讀不到。

緊接著事務B提交事務,然後事務A重新select會開啟一個新的檢視,得到如下圖:

當事務A沿著undo log鏈條往下查詢時,他發現首先發現的name = jerry的行的trx_id是62,竟然比自己的事務ID61還大,但是進一步發現,這個事務ID62並不在trx_ids中。說明,這個其實是已經被提交了的資料,那直接就意味著其實自己是允許讀出這條資料的。這也就是所謂的讀已提交機制。

相關文章