MySQL中的MVCC實現機制

欢乐豆123發表於2024-03-25

MySQL中的MVCC實現機制

一、什麼是MVCC?

MVCC,全稱Multi-Version Concurrency Control,即多版本併發控制。MVCC是一種無鎖的併發控制方法,一般在資料庫管理系統中,用於實現對資料庫的併發訪問。

我們知道,在資料庫中,對資料的操作主要有2中,分別是讀和寫,而在併發場景下,就可能出現以下三種情況:

  • 讀-讀併發
  • 讀-寫併發
  • 寫-寫併發

我們都知道,在沒有寫的情況下發讀-讀併發是不會出現問題的,而寫-寫併發這種情況比較常用的就是透過加鎖的方式實現。那麼,讀-寫併發則可以透過MVCC的機制解決。MVCC在MySQL InnoDB中的實現主要是為了提高資料庫併發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀。本文就來介紹下一下MySQL中MVCC的實現機制。

二、快照讀和當前讀

要想搞清楚MVCC的機制,最重要的一個概念那就是快照讀。

1. 快照讀

所謂快照讀,就是讀取的是快照資料,即快照生成的那一刻的資料,像我們常用的普通的SELECT語句在不加鎖情況下就是快照讀。如:

SELECT * FROM xx_table WHERE ...

2. 當前讀

和快照讀相對應的另外一個概念叫做當前讀,當前讀就是讀取最新資料,所以,加鎖的 SELECT,或者對資料進行增刪改都會進行當前讀,比如:

   SELECT * FROM xx_table LOCK IN SHARE MODE;
   SELECT * FROM xx_table FOR UPDATE;
   INSERT INTO xx_table ...
   DELETE FROM xx_table ...
   UPDATE xx_table ...

可以說快照讀是MVCC實現的基礎,而當前讀是悲觀鎖實現的基礎。

那麼,快照讀讀到的快照是從哪裡讀到的的呢?換句話說,快照是存在哪裡的呢?

三、undo log

undo log是Mysql中比較重要的事務日誌之一,顧名思義,undo log是一種用於回退的日誌,在事務沒提交之前,MySQL會先記錄更新前的資料到 undo log日誌檔案裡面,當事務回滾時或者資料庫崩潰時,可以利用 undo log來進行回退。

這裡面提到的存在 undo log 中的”更新前的資料”就是我們前面提到的快照。所以,這也是為什麼說 undo log 是MVCC實現的重要手段的原因。

那麼,一條記錄在同一時刻可能有多個事務在執行,那麼,undo log會有一條記錄的多個快照,那麼在這一時刻發生SELECT要進行快照讀的時候,要讀哪個快照呢?

這就需要用到另外幾個資訊了

四、隱式欄位

其實,資料庫中的每行記錄中,除了儲存了我們自己定義的一些欄位以外,還有一些重要的隱式欄位的:

1. db_row_id

隱藏主鍵,如果我們沒有給這個表建立主鍵,那麼會以這個欄位來建立聚簇索引。
2. db_trx_id

對這條記錄做了最新一次修改的事務的ID(6位元組)
3. db_roll_ptr

回滾指標(7位元組),指向這條記錄的上一個版本,其實他指向的就是Undo Log中的上一個版本的快照的地址。
因為每一次記錄變更之前都會先儲存一份快照到undo log中,那麼這幾個隱式欄位也會跟著記錄一起儲存在undo log中,就這樣,每一個快照中都有有一個db_trx_id欄位記錄了本次變更的事務ID,以及一個db_roll_ptr欄位指向了上一個快照的地址。(db_trx_id和db_roll_ptr是重點,後面還會用到),這樣,就形成了一個快照連結串列。

有了undo log,又有了幾個隱式欄位,我們好像還是不知道具體應該讀取哪個快照,那怎麼辦呢?

五、Read View

這時候就需要Read View 登場了。Read View 主要來幫我們解決可見性的問題的, 即他會來告訴我們本次事務應該看到哪個快照,不應該看到哪個快照。

1. 在 Read View 中有幾個重要的屬性

1) trx_ids,系統當前未提交的事務 ID 的列表。

2) low_limit_id,未提交的事務中最大的事務 ID。

3) up_limit_id,未提交的事務中最小的事務 ID。

4) creator_trx_id,建立這個 Read View 的事務 ID。

每開啟一個事務,我們都會從資料庫中獲得一個事務 ID,這個事務 ID 是自增長的,透過 ID 大小,我們就可以判斷事務的時間順序。

2. 那麼,一個事務應該看到哪些快照,不應該看到哪些快照該如何判斷呢?

其實原則比較簡單,那就是事務ID大的事務應該能看到事務ID小的事務的變更結果,反之則不能!

我們前面說過,每一條記錄上都有一個隱式欄位db_trx_id記錄對這條記錄做了最新一次修改的事務的ID。那麼接下來,資料庫會拿這條記錄db_trx_id和Read View進行可見性比較。

1) db_trx_id < up_limit_id

這種情況說明,表示這個版本的記錄是在建立 Read View 前已經提交的事務生成的,所以該版本的記錄對當前事務可見.

2) db_trx_id>=low_limit_id
這種情況說明,表示這個版本的記錄是在建立 Read View 後才啟動的事務生成的,所以該版本的記錄對當前事務不可見(不可見怎麼辦呢?後面講)

3) up_limit_id < = db_trx_id < low_limit_id

這種情況下,會再拿db_trx_id和Read View中的trx_ids進行逐一比較。

如果db_trx_id 在trx_ids列表中,表示生成該版本記錄的活躍事務依然活躍著(還沒提交事務),所以該版本的記錄對當前事務不可見。

如果db_trx_id不在trx_ids列表中,表示生成該版本記錄的活躍事務已經被提交,所以該版本的記錄對當前事務可見。

所以,當讀取一條記錄的時候,經過以上判斷,發現記錄對當前事務可見,那麼就直接返回就行了。那麼如果不可見怎麼辦?沒錯,那就需要用到undo log了。

當資料的事務ID不符合Read View規則時候,那就需要從undo log裡面獲取資料的歷史快照,然後資料快照的事務ID再來和Read View進行可見性比較,如果找到一條快照,則返回,找不到則返回空。

所以,總結一下,在InnoDB中,MVCC就是透過Read View + Undo Log來實現的,undo log中儲存了歷史快照,而Read View 用來判斷具體哪一個快照是可見的。

六、MVCC和隔離級別

其實,根據不同的事務隔離級別,Read View的獲取時機是不同的,在RC(Read Commited)下,一個事務中的每一次SELECT都會重新獲取一次Read View,而在RR( Repeatable Read)下,一個事務中只在第一次SELECT的時候會獲取一次Read View。

所以,可重複讀這種事務隔離級別之下,因為有MVCC機制,就可以解決不可重複讀的問題,因為他只有在第一次SELECT的時候才會獲取一次Read View,天然不存在重複讀的問題了。

七、總結

總之,MVCC就是因為設計者不滿意只讓資料庫採用悲觀鎖這樣效能不佳的形式去解決讀-寫衝突問題,而提出的解決方案,所以在資料庫中,因為有了MVCC,所以我們可以形成兩個組合:

1. MVCC + 悲觀鎖 MVCC解決讀寫衝突,悲觀鎖解決寫寫衝突

2. MVCC + 樂觀鎖 MVCC解決讀寫衝突,樂觀鎖解決寫寫衝突

這種組合的方式就可以最大程度的提高資料庫併發效能,並解決讀寫衝突,和寫寫衝突導致的問題

參考連結:

https://www.51cto.com/article/719614.html
https://xiaolincoding.com

相關文章