1. 程式人生 > 其它 >MySql MVCC是如何實現的-MVCC多版本併發控制?

MySql MVCC是如何實現的-MVCC多版本併發控制?

什麼是MVCC?

MVCC在MySQL InnoDB中的實現主要是為了提高資料庫併發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀

什麼是當前讀和快照讀?

當前讀是指讀取的永遠是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種當前讀
快照讀是指事務在啟動的時候就“拍了一個快照”,快照的實現是基於每條資料都存在多個版本。快照讀讀到的不一定是最新的版本,可能會讀到歷史版本。不加鎖的select操作就是快照讀

MVCC的實現原理

隱式欄位

每行記錄除了我們自定義的欄位外,還有資料庫隱式定義的欄位:

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務
  • DB_ROLL_PTR
    7byte,回滾指標,指向這條記錄的上一個版本(儲存於rollback segment裡)
  • DB_ROW_ID
    6byte,隱含的自增ID(隱藏主鍵),如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引
  • 實際還有一個刪除flag隱藏欄位, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了

undo log

InnoDB裡邊每行資料都會存在多個版本。每次事務更新資料的時候,都會生成一個新的資料版本,並且新的資料版本可以通過DB_ROLL_PTR找到舊的資料版本。
如圖所示:就是一個記錄被多個事務連續更新後的狀態。

圖中的黃色區域就是undo log;圖中展示了同一行資料的5個版本,當前最新的版本為V5,value=5,它是被transaction id為10的事務更新的。

V1~V4並不是物理上真是存在的,而是每次需要的時候根據當前版本和undo log計算出來的。

read view

按照可重複讀的定義,一個事務啟動的時候,能夠看到所有已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。

因此,一個事務只需要在啟動的時候宣告說,“以我啟動的時刻為準,如果一個數據版本是在我啟動之前生成的,就認;如果是我啟動以後才生成的,我就不認,我必須要找到它的上一個版本”。

當然,如果“上一個版本”也不可見,那就得繼續往前找。還有,如果是這個事務自己更新的資料,它自己還是要認的。

在實現上, InnoDB 為每個事務構造了一個數組,用來儲存這個事務啟動瞬間,當前正在“活躍”的所有事務 ID。“活躍”指的就是,啟動了但還沒提交。數組裡面事務 ID 的最小值記為低水位,當前系統裡面已經建立過的事務 ID 的最大值加 1 記為高水位。

這個檢視陣列和高水位,就組成了當前事務的一致性檢視(read-view)。而資料版本的可見性規則,就是基於資料的 db_trx_id 和這個一致性檢視的對比結果得到的。

MVCC實現的整體流程

1.理解begin/start transcation和start transaction with consistent snapshot的區別

為什麼要先理解這兩個命令的區別呢?因為後面我們的演示用例會用到。

事務A 事務B
start transacton
update keyvalue set value=2 where key=1;
select * from keyvalue;
  • 根據上一篇的結論,MySql預設的事務隔離級別為可重複讀,autocommit=1,然後按照上圖中的步驟依次執行,從結果可以看出來start transaction命令並沒有真正的開啟一個事務,因為事務B執行更新之後,事務A直接查到了事務B的更新,按照事務隔離級別為可重複讀,開啟事務A之後,事務A應該是讀不到事務B的更新的。

猜想:start transaction命令並不是一個事務的起點,在執行到它之後的第一個操作InnoDB表的語句,事務才真正的啟動。
按照這個猜想繼續往下執行4和5,果然,這一次事務B的更新,在事務A中查不到了

那麼,如果想要馬上啟動一個事務,可以使用start transaction with consistent snapshot命令

2.db_trx_id 和一致性檢視的對比過程
事務A 事務B 事務C
start transaction with consistent snapshot;
start transaction with consistent snapshot;
update keyvalue set value=value+1 where key=1;
update keyvalue set value=value+1 where key=1;
select value from keyvalue where key=1;
select value from keyvalue where key=1;
commit;

假設在執行這三個事務之前,key=1對應的value=1,分析事物A的語句返回的結果是什麼?為什麼?
這裡我們不妨假設:
1.事務A開始前,系統裡邊只有一個活躍的事務ID是99;
2.事務A,B,C的版本號分別是100,101,102,且當前系統中只有這四個事務;
3.三個事務開始前,(1,1)這行資料的db_trx_id是90

這樣事務A的檢視陣列就是[99,100],事務B的檢視陣列是[99,100,101],事務C的檢視陣列是[99,100,101,102]

從圖中可以看到,第一個有效更新的事務C,把資料從(1,1)改成(1,2)。這時候,這個資料的最新版本的db_trx_id是102,而90這個版本已經成為了歷史版本

第二個有效更新是事務B,把資料從(1,2)變成了(1,3)。這時候,這個資料的最新版本的db_trx_id是101,而102又成為了歷史版本。

現在事務A要來讀資料了,它的檢視陣列是[99,100]。

  • 找到(1,3),判斷db_trx_id=101,大於100,不可見
  • 接著,找到上一個歷史版本,一看db_trx_id=102,大於100,不可見
  • 再往前找,終於找到了(1,1),它的db_trx_id=90,小於99,可見

這樣執行下來,雖然期間這一行資料被修改過,但是事務A不論在什麼時候查詢,看到的這行資料的結果,都是一致的,所以我們稱之為一致性讀。

3.更新邏輯與當前讀

細心的同學可能會有疑問:
事務 B 的 update 語句,如果按照一致性讀,好像結果不對哦?
事務B的檢視陣列是先生成的,之後事務C才提交,不是應該看不見(1,2)嗎?怎麼能算出來(1,3)來?

是的,如果事務 B 在更新之前查詢一次資料,這個查詢返回的value的值確實是 1。

但是,當它要去更新資料的時候,就不能再在歷史版本上更新了,否則事務C的更新就丟失了。因此,事務B此時的set value=value+1 是在(1,2)的基礎上進行的操作。

所以,這裡就用到了這樣一條規則:更新資料都是先讀後寫的,而這個讀,只能讀當前的值,稱為“當前讀”(current read)

因此,在更新的時候,當前讀拿到的資料是(1,2),更新後生成了新版本的資料(1,3),這個新版本的 db_trx_id是101。所以,在執行事務B查詢語句的時候,一看自己的版本號是101,最新資料的版本號也是101,是自己的更新,可以直接使用,所以查詢得到的value的值是 3。

這裡我們提到了一個概念,叫作當前讀。

其實,除了update語句外,select語句如果加鎖,也是當前讀。所以,如果把事務A的查詢語句select value from keyvalue where key=1;修改一下,加上lock in share mode 或 for update,也都可以讀到版本號是 101 的資料,返回的value的值是3。下面這兩個 select語句,就是分別加了讀鎖(S 鎖,共享鎖)和寫鎖(X 鎖,排他鎖)。

select `value` from keyvalue where `key`=1 lock in share mode;
select `value` from keyvalue where `key`=1 for update;

特別感謝:
http://gk.link/a/10w98
https://www.jianshu.com/p/8845ddca3b23

牛人之所以是牛人,是因為你現在在踩的坑,他曾經都已經踩過了。