值得收藏,揭秘 MySQL 多版本併發控制實現原理

ITPUB社群 發表於 2022-12-01
MySQL


MySQL 中多版本併發控制(MVCC),是現代資料庫引擎實現中常用的處理讀寫衝突的手段,MVCC 作為 MySQL 高階應用特性,目的在於提高資料庫高併發場景下的吞吐效能。

一、MVCC出現背景是什麼?

事務的4個隔離級別以及對應的3種異常:
值得收藏,揭秘 MySQL 多版本併發控制實現原理

  • 髒讀:一個事務讀取到了另外一個事務沒有提交的資料;

  • 不可重複讀:在同一事務中,兩次讀取同一資料,得到內容不同;

  • 幻讀:同一事務中,用同樣的操作讀取兩次,得到的記錄數不相同。

在 MySQL 中,預設的隔離級別是可重複讀,可以解決髒讀和不可重複讀的問題,但不能解決幻讀問題。如果我們想要解決幻讀問題,就需要採用序列化的方式,也就是將隔離級別提升到最高,但這樣一來就會大幅降低資料庫的事務併發能力。
而MVCC就是透過樂觀鎖的方式來解決不可重複讀和幻讀問題,它可以在大多數情況下替代行級鎖,降低系統的開銷。
MySQL 併發事務會引起更新丟失問題,解決辦法是鎖,主要分兩類:

  • 樂觀鎖:
    其實現如同它的名字一樣,是假設比較好的情況。

    每次取資料的時候都認為他人不會對其修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號機制和CAS演算法實現。
  • 悲觀鎖:
    悲觀鎖也如同它的名字一樣,總是假設比較壞的情況,每次取資料的時候都認為他人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。


二、什麼是MVCC,它解決了什麼問題?

MVCC 是透過資料行的多個版本管理來實現資料庫的併發控制,簡單來說它的思想就是儲存資料的歷史版本。

我們可以透過比較版本號決定資料是否顯示出來(具體的規則後面會介紹到),讀取資料的時候不需要加鎖也可以保證事務的隔離效果。

透過 MVCC 我們可以解決以下幾個問題:
(1)讀寫之間阻塞的問題,透過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務併發處理能力。
(2)降低了死鎖的機率。這是因為 MVCC 採用了樂觀鎖的方式,讀取資料時並不需要加鎖,對於寫操作,也只鎖定必要的行。
(3)解決一致性讀的問題。一致性讀也被稱為快照讀,當我們查詢資料庫在某個時間點的快照時,只能看到這個時間點之前事務提交更新的結果,而不能看到這個時間點之後事務提交的更新結果。
解釋一下可能難以理解的幾個詞彙:

  • 快照讀:
    讀取的是快照資料,不加鎖的簡單的SELECT都屬於快照讀(只是普通的讀操作)。
  • 當前讀:
    當前讀就是讀取最新資料,而不是歷史版本的資料。

    加鎖的SELECT,或者對資料進行增刪改都會進行當前讀(包括加鎖的讀取和DML操作)。


三、應用舉例分析

為了更好地讓大家理解MVCC,我們用一個示例場景來說明。
假設有個賬戶金額表 user_balance,包括三個欄位,分別是 username 使用者名稱、balance 餘額和 bankcard 卡號,表資料如下所示:
值得收藏,揭秘 MySQL 多版本併發控制實現原理
使用者 A 和使用者 B 之間進行轉賬,此時資料庫管理員想要查詢 user_balance 表中的總金額,兩個場景存在併發情況,在沒有MVCC的情況下,會出現哪些問題呢。
Case1:因為需要採用加行鎖的方式,使用者 A 給 B 轉賬時間等待很久,如下圖所示。
值得收藏,揭秘 MySQL 多版本併發控制實現原理
Case2:當我們讀取的時候用了加行鎖,可能會出現死鎖的情況,如下圖所示。
值得收藏,揭秘 MySQL 多版本併發控制實現原理
比如當我們讀到 A 有 1000 元的時候,此時 B 開始執行給 A 轉賬。

四、InnoDB如何實現MVCC?

當查詢一條記錄的時候,執行流程如下:

  1. 首先獲取事務自己的版本號,也就是事務 ID;

  2. 獲取 Read View;

  3. 查詢得到的資料,然後與 Read View 中的事務版本號進行比較;

  4. 如果不符合 ReadView 規則,就需要從 Undo Log 中獲取歷史快照;

  5. 最後返回符合規則的資料。


相關概念
1. 事務版本號
一個自增長的事務ID,用於標記事務執行的先後順序。
2. Read View
在 MVCC 機制中,多個事務對同一個行記錄進行更新會產生多個歷史快照,這些歷史快照儲存在 Undo Log 裡。如果一個事務想要查詢這個行記錄,需要讀取哪個版本的行記錄呢?
這時就需要用到 Read View 了,它幫我們解決了行的可見性問題。Read View 儲存了當前事務開啟時所有活躍(還沒有提交)的事務列表,換個角度,可以理解為 Read View 儲存了不應該讓這個事務看到的其他的事務 ID 列表。
Read VIew 中的幾個重要屬性:
值得收藏,揭秘 MySQL 多版本併發控制實現原理

  • up_limit_id,活躍的事務中最小的事務 ID;

  • trx_ids,系統當前正在活躍的事務 ID 集合;

  • low_limit_id,活躍的事務中最大的事務 ID;

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


3. 行記錄的隱藏列
InnoDB 的葉子節點段儲存了資料頁,資料頁中儲存了行記錄,在這些行記錄中有一些重要的隱藏欄位:

  • DB_ROW_ID :

    6-byte,記錄操作該資料事務的事務ID;

  • DB_TRX_ID :

    6-byte,當建立表沒有合適的索引作為聚集索引時,會用該隱藏ID建立聚集索引;

  • DB_ROLL_PTR :

    7-byte,回滾指標,指向上一個版本資料在undo log 裡的位置指標;


4. 聚集索引
聚集索引是指資料庫錶行中資料的物理順序與鍵值的邏輯(索引)順序相同。一個表只能有一個聚集索引,因為一個表的物理順序只有一種情況,所以,對應的聚集索引只能有一個。
5. Undo Log
InnoDB 將行記錄快照儲存在 Undo Log,可以在回滾段中找到它們,主要用於記錄資料被修改之前的日誌,在對錶資訊做修改之前先會把資料複製到Undo Log裡,當事務進行回滾時可以透過Undo Log裡的日誌進行資料還原。
回滾段中回滾指標間關聯關係,如下圖所示:
值得收藏,揭秘 MySQL 多版本併發控制實現原理五、InnoDB是如何解決幻讀的?
1、在讀已提交的情況下,即使採用了 MVCC 方式也會出現幻讀
值得收藏,揭秘 MySQL 多版本併發控制實現原理

我們同時開啟事務 A 和事務 B,先在事務 A 中進行某個條件範圍的查詢,讀取的時候採用排它鎖,在事務 B 中增加一條符合該條件範圍的資料,並進行提交,然後我們在事務 A 中再次查詢該條件範圍的資料,就會發現結果集中多出一個符合條件的資料,這樣就出現了幻讀。

出現幻讀的原因是在讀已提交的情況下,InnoDB 只採用記錄鎖(Record Locking)。
InnoDB 三種行鎖的方式:

  • 記錄鎖:

    針對單個行記錄新增鎖。

  • 間隙鎖(Gap Locking):

    可以鎖住一個範圍(索引之間的空隙),但不包括記錄本身。

    採用間隙鎖的方式可以防止幻讀情況的產生。

  • Next-Key 鎖:

    鎖住一個範圍,同時鎖定記錄本身,相當於間隙鎖 + 記錄鎖,可以解決幻讀的問題。


2、在可重複讀的情況下,InnoDB 可以透過 Next-Key 鎖 +MVCC 來解決幻讀問題。
想插入球員艾利克斯·倫(身高 2.16 米)的時候,事務 B 會超時,無法插入該資料。
這是因為採用了 Next-Key 鎖,會將 height>2.08 的範圍都進行鎖定,就無法插入符合這個範圍的資料了。然後事務 A 重新進行條件範圍的查詢,就不會出現幻讀的情況。值得收藏,揭秘 MySQL 多版本併發控制實現原理

六、總結

MVCC 的核心就是 Undo Log+ Read View。

  • “MV”就是透過 Undo Log 來儲存資料的歷史版本,實現多版本的管理;

  • “CC”是透過 Read View 來實現管理,透過 Read View 原則來決定資料是否顯示。

同時針對不同的隔離級別,Read View 的生成策略不同,也就實現了不同的隔離級別。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2926163/,如需轉載,請註明出處,否則將追究法律責任。