前言
????????多版本并發控制(MVCC)是 MySQL InnoDB 存儲引擎實現高性能事務的核心機制。它通過創建數據快照,使得讀寫操作可以無鎖并發,極大地提升了數據庫的并發性能。本文將深入探討 MVCC 的工作原理、實現細節以及它與事務隔離級別的緊密關系。
一、 MVCC 要解決什么問題?
????????在高并發場景下,數據庫事務處理主要面臨三種操作組合:
-
讀-讀:無需任何控制,不會產生問題。
-
寫-寫:必須通過加鎖(行鎖、表鎖等)實現串行化,保證數據一致性。
-
讀-寫:如果采用加鎖的方式,讀操作會阻塞寫操作,寫操作也會阻塞讀操作,嚴重影響并發性能。
MVCC 的終極目標就是優雅地解決 讀-寫沖突,實現無鎖的非阻塞并發讀。
二、 MVCC 的實現基石
????????MVCC 的實現依賴于兩個核心部分:數據的版本鏈 和 事務的讀視圖 (Read View)。
-
數據的版本鏈 (Undo Log) InnoDB 的每行記錄中都包含兩個重要的隱藏字段:
-
DB_TRX_ID (6字節):記錄最后一次插入或更新該行數據的事務 ID。
-
DB_ROLL_PTR (7字節):回滾指針,指向該行數據的前一個版本(存儲在 Undo Log 中)。 每次對記錄進行更新時,都會將舊值寫入 Undo Log,然后
DB_ROLL_PTR
會形成一個指向舊版本記錄的鏈表,即版本鏈。鏈首是最新的記錄,鏈尾是最舊的記錄。
-
-
事務的讀視圖 (Read View) Read View 是事務在執行快照讀(普通 SELECT 語句)時產生的讀視圖,它決定了當前事務能看到哪個版本的數據。 它主要包含以下關鍵屬性:
-
m_ids:創建 Read View 時,系統中所有活躍(尚未提交)事務的事務 ID 集合。
-
min_trx_id:m_ids 集合中的最小值。
-
max_trx_id:創建 Read View 時,系統尚未分配的下一個事務 ID(并非
m_ids
中的最大值)。 -
creator_trx_id:創建該 Read View 的事務的事務 ID。
-
三、 可見性算法
????????有了版本鏈和 Read View,就可以根據以下規則判斷某個版本的記錄是否對當前事務可見:
-
如果數據版本的
DB_TRX_ID
小于min_trx_id
,說明該版本在 Read View 創建前已提交,可見。 -
如果數據版本的
DB_TRX_ID
大于等于max_trx_id
,說明該版本是由在 Read View 創建之后才啟動的事務生成的,不可見。需要沿著版本鏈繼續查找舊版本。 -
如果數據版本的
DB_TRX_ID
在[min_trx_id, max_trx_id)
區間內:-
若
DB_TRX_ID
在m_ids
集合中,說明創建 Read View 時該事務仍活躍,其修改不可見。 -
若
DB_TRX_ID
不在m_ids
集合中,說明創建 Read View 時該事務已提交,其修改可見。
-
-
如果當前記錄對自己的事務做了修改(
DB_TRX_ID == creator_trx_id
),那么該版本總是可見的。
四、 MVCC 與隔離級別
????????MVCC 的行為因事務隔離級別而異,核心區別在于 Read View 的生成時機:
-
READ COMMITTED (讀已提交):每次執行快照讀時都會生成一個新的 Read View。這會導致每次讀都能看到其他事務已提交的最新修改,從而產生“不可重復讀”現象。
-
REPEATABLE READ (可重復讀):只在第一次執行快照讀時生成一個 Read View,后續所有讀操作都復用這個視圖。這就保證了在整個事務過程中,看到的數據內容是一致的,實現了可重復讀。
五、說人話(個人理解)
????????對于Mysql的MVCC他解決的問題是在事務并發情況下,對于讀+寫操作的無鎖解決方案。
????????在事務并發場景下會出現讀+讀、讀+寫、寫+寫這三種組合,其中對于讀+讀是無需干預的,能夠保證并發場景下數據的一致性以及隔離性。對于寫+寫操作就需要按照一定次序串行執行了,對于該問題就需要鎖來實現,如果不加鎖的話是無法保證數據的一致性以及隔離性的。這個問題不在MVCC討論的范疇中,主要解決方式與Mysql的鎖相關。此處不再贅述。對于讀+寫操作在解決方案上可以通過鎖來保證數據的一致性但是加鎖就會導致鎖的競爭問題進而影響整體Mysql的并發度問題導致命令執行效率下降。此時MVCC機制就實現了無鎖的情況下舍棄數據實時性為代價提高事務并發效率。因為在一定場景下可以接受數據出現一定程度的不一致問題,因此可以犧牲此部分來追求并發度的提升。
????????對于MVCC機制他的實現基礎,即立足點是針對數據表中的數據進行不同版本的控制,針對數據不同版本判斷各個事務對于數據的可見性。即事務對應的版本對應所查詢的數據的版本是否合理。進而引出了快照讀這個操作。上述兩個關鍵點:”事務的版本”和“數據的版本”是MVCC機制實現的基礎。
????????其中數據的版本通過每行數據中幾個隱藏字段進行標識:trx_id字段標識了最后一次修改/增加改行數據的事務id。roll_pointer字段指向該行數據上個版本的數據信息。在事務執行過程中會將這些信息記錄到undo log日志中,通過該日志可以實現事務回滾以及MVCC。
????????事務的版本控制通過建立readview來控制。readview包含信息為:m_ids記錄了當前readview創建時刻所有活躍的事務(已創建但是未提交的事務)。min_trx_id記錄創建readview時最小的活躍事務id。max_trx_id記錄創建readview時最大的活躍事務id。create_trx_id記錄創建readview時分配給當前事務的id,該字段為全局自增字段。通過trx_id和roll_pointor字段可以對數據建立一個版本鏈。然后每個事務的readview為該事務創建時刻對當前數據庫事務處理情況的一個快照,通過對比當前事務需要查詢的數據版本鏈信息可以得出當前事務可見的數據版本。
????????這個具體的規則為:當數據trx_id小于事務readview的min_trx_id時,證明此時數據行最后一次修改的事務是在當前事務創建之前就已經提交了,所以是合理的,因此是對于該事務而言是可見的。當trx_id大于事務readview的max_trx_id時,證明此時數據行最后一次修改的事務是在事務創建之后又新建的事務,此時在時間線上該數據行的版本是先于當前事務的,所以該數據行對于事務而言是不可見的,需要通過roll_pointor字段查詢舊版本的數據行。如果trx_id位于readview的min_trx_id和max_trx_id之間時,證明此數據行最后一次修改的事務是與當前事務是位于同一時間線的,證明是同一版本,但是修改該數據行的事務是否提交這個還需要判斷,因此進一步判斷該trx_id是否存在于m_ids列表中,如果存在,證明這兩個事務都是活躍的,此時不能保證該數據行是否修改完成,所以是不可見的。而如果不存在m_ids列表中時,證明修改該數據行的事務已經提交,此時是可見的。
????????而對應MVCC的控制也有級別之分。其中就是可重復讀級別和讀已提交級別。 對于兩者的區別是兩者的readview創建時機不同。其中讀已提交級別他的readview創建是在每一次快照讀時都會重新創建,更新其中的m_ids等字段,此時他的并發程度更高,因為該級別每一次快照讀都會放大數據行的版本可見區間。但是會在數據一致性上帶來不可重復讀的問題,也就是一個事務兩次讀取同一數據不一致的問題。因為他在每次快照讀時都會重建readview,如果第一次和第二次查詢期間有其他事務提交對任務的修改,此時就會出現不可重復讀的問題。而可重復讀的readview創建時機是在事務第一次創建時進行創建,后續不會新建。此時對于可重復讀隔離級別而言他的快照讀操作就定格在了事務創建的那一刻了,此時就算有其他事務在第一次查詢和第二次查詢期間他都只會查詢到第一次的數據行,因為第二次的數據行對于該隔離級別而言是不可見的。但是也會有一定問題就是幻讀問題。
?更多資料:0voice · GitHub