MySQL InnoDB MVCC 機制的原理及實現

什麼是 MVCC

MVCC (Multiversion Concurrency Control) 中文全程叫多版本併發控制,是現代數據庫(包括 MySQL、Oracle、PostgreSQL 等)引擎實現中常用的處理讀寫衝突的手段,目的在於提高數據庫高併發場景下的吞吐性能

如此一來不同的事務在併發過程中,SELECT 操作可以不加鎖而是通過 MVCC 機制讀取指定的版本歷史記錄,並通過一些手段保證保證讀取的記錄值符合事務所處的隔離級別,從而解決併發場景下的讀寫衝突。

下面舉一個多版本讀的例子,例如兩個事務 A 和 B 按照如下順序進行更新和讀取操作


MySQL InnoDB MVCC 機制的原理及實現


在事務 A 提交前後,事務 B 讀取到的 x 的值是什麼呢?答案是:事務 B 在不同的隔離級別下,讀取到的值不一樣。

  1. 如果事務 B 的隔離級別是讀未提交(RU),那麼兩次讀取均讀取到 x 的最新值,即 20。
  2. 如果事務 B 的隔離級別是讀已提交(RC),那麼第一次讀取到舊值 10,第二次因為事務 A 已經提交,則讀取到新值 20。
  3. 如果事務 B 的隔離級別是可重複讀或者串行(RR,S),則兩次均讀到舊值 10,不論事務 A 是否已經提交。

可見在不同的隔離級別下,數據庫通過 MVCC 和隔離級別,讓事務之間並行操作遵循了某種規則,來保證單個事務內前後數據的一致性。

為什麼需要 MVCC

InnoDB 相比 MyISAM 有兩大特點,一是支持事務而是支持行級鎖,事務的引入帶來了一些新的挑戰。相對於串行處理來說,併發事務處理能大大增加數據庫資源的利用率,提高數據庫系統的事務吞吐量,從而可以支持可以支持更多的用戶。但併發事務處理也會帶來一些問題,主要包括以下幾種情況:

  1. 更新丟失(Lost Update):當兩個或多個事務選擇同一行,然後基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題 —— 最後的更新覆蓋了其他事務所做的更新。如何避免這個問題呢,最好在一個事務對數據進行更改但還未提交時,其他事務不能訪問修改同一個數據。
  2. 髒讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些尚未提交的髒數據,並據此做進一步的處理,就會產生未提交的數據依賴關係。這種現象被形象地叫做 “髒讀”
  3. 不可重複讀(Non-Repeatable Reads):一個事務在讀取某些數據已經發生了改變、或某些記錄已經被刪除了!這種現象叫做“不可重複讀”。
  4. 幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為 “幻讀”

以上是併發事務過程中會存在的問題,解決更新丟失可以交給應用,但是後三者需要數據庫提供事務間的隔離機制來解決。實現隔離機制的方法主要有兩種:

  1. 加讀寫鎖
  2. 一致性快照讀,即 MVCC

但本質上,隔離級別是一種在併發性能和併發產生的副作用間的妥協,通常數據庫均傾向於採用 Weak Isolation。

InnoDB 中的 MVCC

本文聚焦於 MySQL 中的 MVCC 實現,從 《高性能 MySQL》一書中對 MVCC 的介紹可知:

  1. MySQL 中 InnoDB 引擎支持 MVCC
  2. 應對高併發事務, MVCC 比單純的加行鎖更有效, 開銷更小
  3. MVCC 在讀已提交(Read Committed)和可重複讀(Repeatable Read)隔離級別下起作用
  4. MVCC 既可以基於樂觀鎖又可以基於悲觀鎖
    來實現

InnoDB MVCC 實現原理

InnoDB 中 MVCC 的實現方式為:每一行記錄都有兩個隱藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果沒有主鍵,則還會多一個隱藏的主鍵列)。


MySQL InnoDB MVCC 機制的原理及實現


DATA_TRX_ID

記錄最近更新這條行記錄的事務 ID,大小為 6 個字節

DATA_ROLL_PTR

表示指向該行回滾段(rollback segment)的指針,大小為 7 個字節,InnoDB 便是通過這個指針找到之前版本的數據。該行記錄上所有舊版本,在 undo 中都通過鏈表的形式組織。

DB_ROW_ID

行標識(隱藏單調自增 ID),大小為 6 字節,如果表沒有主鍵,InnoDB 會自動生成一個隱藏主鍵,因此會出現這個列。另外,每條記錄的頭信息(record header)裡都有一個專門的 bit(deleted_flag)來表示當前記錄是否已經被刪除。

如何組織 Undo Log 鏈

關於 Redo Log 和 Undo Log 的相關概念可見之前的文章 InnoDB 中的 redo 和 undo log

上文提到,在多個事務並行操作某行數據的情況下,不同事務對該行數據的 UPDATE 會產生多個版本,然後通過回滾指針組織成一條 Undo Log 鏈,這節我們通過一個簡單的例子來看一下 Undo Log 鏈是如何組織的,DATA_TRX_ID 和 DATA_ROLL_PTR 兩個參數在其中又起到什麼樣的作用。

還是以上文 MVCC 的例子,事務 A 對值 x 進行更新之後,該行即產生一個新版本和舊版本。假設之前插入該行的事務 ID 為 100,事務 A 的 ID 為 200,該行的隱藏主鍵為 1。


MySQL InnoDB MVCC 機制的原理及實現


事務 A 的操作過程為:

  1. 對 DB_ROW_ID = 1 的這行記錄加排他鎖
  2. 把該行原本的值拷貝到 undo log 中,DB_TRX_ID 和 DB_ROLL_PTR 都不動
  3. 修改該行的值這時產生一個新版本,更新 DATA_TRX_ID 為修改記錄的事務 ID,將 DATA_ROLL_PTR 指向剛剛拷貝到 undo log 鏈中的舊版本記錄,這樣就能通過 DB_ROLL_PTR 找到這條記錄的歷史版本。如果對同一行記錄執行連續的 UPDATE,Undo Log 會組成一個鏈表,遍歷這個鏈表可以看到這條記錄的變遷
  4. 記錄 redo log,包括 undo log 中的修改

那麼 INSERT 和 DELETE 會怎麼做呢?其實相比 UPDATE 這二者很簡單,INSERT 會產生一條新紀錄,它的 DATA_TRX_ID 為當前插入記錄的事務 ID;DELETE 某條記錄時可看成是一種特殊的 UPDATE,其實是軟刪,真正執行刪除操作會在 commit 時,DATA_TRX_ID 則記錄下刪除該記錄的事務 ID。

如何實現一致性讀 —— ReadView

在 RU 隔離級別下,直接讀取版本的最新記錄就 OK,對於 SERIALIZABLE 隔離級別,則是通過加鎖互斥來訪問數據,因此不需要 MVCC 的幫助。因此 MVCC 運行在 RC 和 RR 這兩個隔離級別下,當 InnoDB 隔離級別設置為二者其一時,在 SELECT 數據時就會用到版本鏈

核心問題是版本鏈中哪些版本對當前事務可見?

InnoDB 為了解決這個問題,設計了 ReadView(可讀視圖)的概念。

RR 下的 ReadView 生成

在 RR 隔離級別下,每個事務 touch first read 時(本質上就是執行第一個 SELECT 語句時,後續所有的 SELECT 都是複用這個 ReadView,其它 update, delete, insert 語句和一致性讀 snapshot 的建立沒有關係),會將當前系統中的所有的活躍事務拷貝到一個列表生成ReadView。

下圖中事務 A 第一條 SELECT 語句在事務 B 更新數據前,因此生成的 ReadView 在事務 A 過程中不發生變化,即使事務 B 在事務 A 之前提交,但是事務 A 第二條查詢語句依舊無法讀到事務 B 的修改。


MySQL InnoDB MVCC 機制的原理及實現


下圖中,事務 A 的第一條 SELECT 語句在事務 B 的修改提交之後,因此可以讀到事務 B 的修改。但是注意,如果事務 A 的第一條 SELECT 語句查詢時,事務 B 還未提交,那麼事務 A 也查不到事務 B 的修改。


MySQL InnoDB MVCC 機制的原理及實現


RC 下的 ReadView 生成

在 RC 隔離級別下,每個 SELECT 語句開始時,都會重新將當前系統中的所有的活躍事務拷貝到一個列表生成 ReadView。二者的區別就在於生成 ReadView 的時間點不同,一個是事務之後第一個 SELECT 語句開始、一個是事務中每條 SELECT 語句開始。

ReadView 中是當前活躍的事務 ID 列表,稱之為 m_ids,其中最小值為 up_limit_id,最大值為 low_limit_id,事務 ID 是事務開啟時 InnoDB 分配的,其大小決定了事務開啟的先後順序,因此我們可以通過 ID 的大小關係來決定版本記錄的可見性,具體判斷流程如下:

  1. 如果被訪問版本的 trx_id 小於 m_ids 中的最小值 up_limit_id,說明生成該版本的事務在 ReadView 生成前就已經提交了,所以該版本可以被當前事務訪問。
  2. 如果被訪問版本的 trx_id 大於 m_ids 列表中的最大值 low_limit_id,說明生成該版本的事務在生成 ReadView 後才生成,所以該版本不可以被當前事務訪問。需要根據 Undo Log 鏈找到前一個版本,然後根據該版本的 DB_TRX_ID 重新判斷可見性。
  3. 如果被訪問版本的 trx_id 屬性值在 m_ids 列表中最大值和最小值之間(包含),那就需要判斷一下 trx_id 的值是不是在 m_ids 列表中。如果在,說明創建 ReadView 時生成該版本所屬事務還是活躍的,因此該版本不可以被訪問,需要查找 Undo Log 鏈得到上一個版本,然後根據該版本的 DB_TRX_ID 再從頭計算一次可見性;如果不在,說明創建 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。
  4. 此時經過一系列判斷我們已經得到了這條記錄相對 ReadView 來說的可見結果。此時,如果這條記錄的 delete_flag 為 true,說明這條記錄已被刪除,不返回。否則說明此記錄可以安全返回給客戶端。


MySQL InnoDB MVCC 機制的原理及實現


舉個例子

RC 下的 MVCC 判斷流程

我們現在回看剛剛的查詢過程,為什麼事務 B 在 RC 隔離級別下,兩次查詢的 x 值不同。RC 下 ReadView 是在語句粒度上生成的。

當事務 A 未提交時,事務 B 進行查詢,假設事務 B 的事務 ID 為 300,此時生成 ReadView 的 m_ids 為 [200,300],而最新版本的 trx_id 為 200,處於 m_ids 中,則該版本記錄不可被訪問,查詢版本鏈得到上一條記錄的 trx_id 為 100,小於 m_ids 的最小值 200,因此可以被訪問,此時事務 B 就查詢到值 10 而非 20。

待事務 A 提交之後,事務 B 進行查詢,此時生成的 ReadView 的 m_ids 為 [300],而最新的版本記錄中 trx_id 為 200,小於 m_ids 的最小值 300,因此可以被訪問到,此時事務 B 就查詢到 20。

RR 下的 MVCC 判斷流程

如果在 RR 隔離級別下,為什麼事務 B 前後兩次均查詢到 10 呢?RR 下生成 ReadView 是在事務開始時,m_ids 為 [200,300],後面不發生變化,因此即使事務 A 提交了,trx_id 為 200 的記錄依舊處於 m_ids 中,不能被訪問,只能訪問版本鏈中的記錄 10。

一個爭論點

其實並非所有的情況都能套用 MVCC 讀的判斷流程,特別是針對在事務進行過程中,另一個事務已經提交修改的情況下,這時不論是 RC 還是 RR,直接套用 MVCC 判斷都會有問題,例如 RC 下:


MySQL InnoDB MVCC 機制的原理及實現


事務 A 的 trx_id = 200,事務 B 的 trx_id = 300,且事務 B 修改了數據之後在事務 A 之前提交,此時 RC 下事務 A 讀到的數據為事務 B 修改後的值,這是很顯然的。下面我們套用下 MVCC 的判斷流程,考慮到事務 A 第二次 SELECT 時,m_ids 應該為 [200],此時該行數據最新的版本 DATA_TRX_ID = 300 比 200 大,照理應該不能被訪問,但實際上事務 A 選取了這條記錄返回。

這裡其實應該結合 RC 的本質來看,RC 的本質就是事務中每一條 SELECT 語句均可以看到其他已提交事務對數據的修改,那麼只要該事物已經提交其結果就是可見的,與這兩個事務開始的先後順序無關,不完全適用於 MVCC 讀

RR 級別下還是用之前那張圖:


MySQL InnoDB MVCC 機制的原理及實現


這張圖的流程中,事務 B 的 trx_id = 300 比事務 A 200 小,且事務 B 先於事務 A 提交,按照 MVCC 的判斷流程,事務 A 生成的 ReadView 為 [200],最新版本的行記錄 DATA_TRX_ID = 300 比 200 大,照理不能訪問到,但是事務 A 實際上讀到了事務 B 已經提交的修改。這裡還是結合 RR 本質進行解釋,RR 的本質是從第一個 SELECT 語句生成 ReadView 開始,任何已經提交過的事務的修改均可見。

總結

RC、RR 兩種隔離級別的事務在執行普通的讀操作時,通過訪問版本鏈的方法,使得事務間的讀寫操作得以併發執行,從而提升系統性能。RC、RR 這兩個隔離級別的一個很大不同就是生成 ReadView 的時間點不同,RC 在每一次 SELECT 語句前都會生成一個 ReadView,事務期間會更新,因此在其他事務提交前後所得到的 m_ids 列表可能發生變化,使得先前不可見的版本後續又突然可見了。而 RR 只在事務的第一個 SELECT 語句時生成一個 ReadView,事務操作期間不更新。

  • 《高性能 MySQL》
  • Understanding InnoDB MVCC
  • 15.3 InnoDB Multi-Versioning
  • MySQL · 引擎特性 · InnoDB MVCC 相關實現
  • MySQL事務隔離級別和MVCC
  • MVCC 原理探究及 MySQL 源碼實現分析
  • 深入淺出INNODB MVCC機制與原理


分享到:


相關文章: