多版本並行控制 MVCC

2022-09-14 12:02:54

介紹多版本並行控制

多版本並行控制技術(Multiversion Concurrency Control,MVCC)

技術是為了解決問題而生的,通過 MVCC 我們可以解決以下幾個問題:

  1. 讀寫之間阻塞的問題:通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務並行處理能力。
  2. 降低了死鎖的概率:這是因為 MVCC 沒有使用鎖,讀取資料時並不需要加鎖,對於寫操作,也只鎖定必要的行。
  3. 解決一致性讀的問題:一致性讀也被稱為快照讀,當我們查詢資料庫在某個時間點的快照時,只能看到這個時間點之前事務提交更新的結果,而不能看到這個時間點之後事務提交更新的結果。

MVCC 的思想

MVCC 是通過資料行的歷史版本來實現資料庫的並行控制。

簡單來說 MVCC 的思想就是儲存資料的歷史版本。這樣一個事務進行查詢操作時,就可以通過比較版本號來判斷哪個較新的版本對當前事務可見。

InnoDB 對 MVCC 的實現

MVCC 沒有正式的標準,所以在不同的 DBMS 中,MVCC 的實現方式可能是不同的。

InnoDB 對 MVCC 的實現主要是通過 版本鏈 + ReadView 結構完成。

版本鏈儲存記錄的多個版本

先介紹聚簇索引記錄的隱藏列,再介紹 Undo Log 版本鏈


對於使用 InnoDB 儲存引擎的表來說,它的聚簇索引記錄中都包含 3 個隱藏列

  1. db_row_id:隱藏的行 ID。在沒有自定義主鍵也沒有 Unique 鍵的情況下,會使用該隱藏列作為主鍵。
  2. db_trx_id:操作這個資料的事務 ID,也就是最後一個對該資料進行插入或更新的事務 ID。
  3. db_roll_ptr:回滾指標,也就是指向這個記錄的 Undo Log 資訊。Undo Log 中儲存了回滾需要的資料。

事務ID

事務執行過程中,只有在第一次真正修改記錄時(比如進行 insert、delete、update 操作),才會被分配一個唯一的、單調遞增的事務 ID,如果沒有修改記錄操作,按照一定的策略分配一個比較大的事務 ID,減少分配事務 ID 的鎖競爭。每當事務向資料庫寫入新內容時, 所寫的資料都會被標記操作所屬的事務的事務ID。


在 InnoDB 儲存引擎中,版本鏈由資料行的 Undo Log 組成。

每次對資料行進行修改,都會將舊值記錄到 Undo Log,算是該資料行的一箇舊版本。

Undo Log 有兩個重要的屬性:db_roll_ptr、db_trx_id

  • Undo Log 也有一個 db_roll_ptr 屬性(insert 操作對應的 Undo Log 沒有 db_roll_ptr 屬性,因為 insert 操作對應的資料行沒有更早的版本),Undo Log 的 db_roll_ptr 屬性指向上一次操作的 Undo Log,所有的版本被 db_roll_ptr 屬性連線形成一個連結串列。該連結串列即版本鏈,版本鏈的頭節點就是資料行的最新值。

  • Undo Log 還包含生成該版本時,對應的事務 ID,用於判斷當前版本的資料對事務的可見性。

版本鏈如下圖所示。這樣如果我們想要查詢歷史快照,就可以通過遍歷回滾指標的方式進行查詢。

ReadView 判斷版本鏈中的哪個較新的版本對當前事務是可見的

ReadView 用來判斷版本鏈中的哪個較新的版本對當前事務是可見的。

ReadView 中主要包含 4 個比較重要的屬性:

  • m_ids:表示在生成 ReadView 時,當前系統中所有活躍的讀寫事務的 ID 集合(列表)
  • min_transaction_id:表示在生成 ReadView 時,m_ids 中的最小值
  • max_transaction_id:表示在生成 ReadView 時,系統應該分配給下一個事務的 ID 值
  • creator_transaction_id:表示生成該 ReadView 的事務的 ID

有了這個 ReadView,這樣在存取某條記錄時,就可以用 ReadView 來判斷版本鏈中的哪個較新的版本對當前事務是可見的。

  • 如果被存取版本的 transaction_id 屬性值與 ReadView 中的 creator_trx_id 值相同,表明當前事務在存取它自己修改過的記錄,所以該版本可以被當前事務存取。

  • 如果被存取版本的 transaction_id 屬性值 小於 ReadView 中的 min_trx_id 值,表明生成該版本的事務在當前事務生成 ReadView 前已經提交了,所以該版本可以被當前事務存取。

  • 如果被存取版本的 transaction_id 屬性值 大於 ReadView 中的 max_trx_id 值,表明生成該版本的事務在當前事務生成 ReadView 後才開啟,所以該版本不可以被當前事務存取。

  • 如果被存取版本的 transaction_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id 之間,那就需要判斷一下 transaction_id 屬性值是不是在 m_ids 列表中:

    • 如果在,表明生成 ReadView 時,被存取版本的事務還是活躍的,所以該版本不可以被當前事務存取
    • 如果不在,表明生成 ReadView 時,被存取版本的事務已經被提交了,所以該版本可以被當前事務存取

如果某個版本的資料對當前事務不可見的話,那就順著版本鏈找到下一個版本的資料,繼續按照上邊的步驟判斷

可見性,依此類推,直到版本鏈中的最後一個版本。如果最後一個版本也不可見的話,那麼就意味著該條記錄對當前事務完全不可見,查詢結果就不包含該記錄。

ReadView 的生成時機

MVCC 可以防止髒讀,也可以防止不可重複讀。

防止髒讀 和 防止不可重複讀 實現的不同之處就在:ReadView 的生成時機不同

  • 防止髒讀:每次讀取資料前,都生成一個 ReadView
  • 防止不可重複讀:在當前事務第一次讀取資料時,生成一個 ReadView,之後的查詢操作都重複使用這個 ReadView

對於隔離級別為 讀未提交 的事務來說,直接讀取記錄的最新版本即可。

對於隔離級別為 序列化 的事務來說,InnoDB 儲存引擎使用加鎖的方式來存取記錄。

對於隔離級別為 讀已提交 和 可重複讀 的事務來說,都必須保證只能讀到已經提交的事務修改的資料,不能讀到未提交的事務修改的資料。

參考資料

MySQL 是怎樣執行的:從根兒上理解 MySQL - 小孩子4919 - 掘金課程 (juejin.cn)