我勸!這位年輕人不講MVCC,耗子尾汁!
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中。說明,這個其實是已經被提交了的數據,那直接就意味著其實自己是允許讀出這條數據的。這也就是所謂的讀已提交機制。
四、長事物的風險
其實文章看到這裡,長事物有什麼風險你應該也可以感覺出來了。
事務遲遲不結束,就意味著它隨時可能會訪問到資料庫中任何數據,所以只要是它們可能用的回滾記錄,資料庫都得為他們保留著。所以事物越長,相應的他對應的視圖也就越大。
上一篇文章中白日夢有和大家介紹過 undo log 默認存放在共享表空間文件中,同時在SQL5.6 MySQL5.7在也允許你將undo log拿到單獨的表空間中去,但是不論怎樣,undo log總會以真實存在的文件的形式存在於磁碟上,當然了MySQL5.7的undo truncate機制 結合purge執行緒可以將不需要的undo log清除掉,為undo log文件瘦身,但是在這之前undo log的體量會不斷的增大,再加上大量的長事物,很可能會將磁碟打爆。
另外長事物大概率是update等DML導致的,這種DML是會持有行鎖的。誰也不能保證長時間不釋放鎖不會導致資料庫被拖垮。
本文是MySQL專題第15篇,全文近100篇(公眾號首發)
本文是第15篇,全文近100篇,點擊查看目錄