InnoDB的多版本併發控制機制—— MVCC底層實現

什麼是MVCC?

MVCC是Multi-Version Concurrency Control(多版本併發控制)的縮寫,MVCC沒有統一的實現標準,不同的存儲引擎對MVCC的實現方式是不同的,典型的有樂觀併發控制和悲觀併發控制。InnoDB對MVCC的實現採用的是樂觀併發控制。

InnoDB-MVCC如何實現?

在《高性能MySQL》一書中,關於InnoDB-MVCC的實現是這樣介紹的:

InnoDB的MVCC,是通過在每行記錄後保存兩個隱藏的列來實現的(用戶不可見)。一個列保存行創建的時間,一個列保存行過期(刪除)的時間,這裏所說的時間並不是傳統意義上的時間,而是系統版本號,下面是REPEATABLE READ隔離級別下MVCC的具體操作:
- SELECT
InnoDB會根據以下兩個條件檢查每行記錄:
(1)InnoDB只查找版本早於當前事務版本的數據行(行的系統版本號小於或者等於事務的系統版本號),這樣可以確保事務讀取到的行,要麼是在事務開始之前已經存在的,要麼是事務自身插入或者修改過的;
(2)行的刪除版本要麼未定義,要麼大於當前事務版本號。可以確保事務讀取到的行,在事務開啓之前未被刪除。
- INSERT
InnoDB爲新插入的每一行保存當前系統版本號作爲行版本號。
- DELETE
InnoDB爲刪除的每一行保存當前系統版本號作爲行刪除標識。
- UPDATE
InnoDB將更新後的列作爲新的行插入數據表,並保存當前系統版本號作爲該行的行版本號,同時保存當前系統版本號到原來的行作爲行刪除標識。

看完書中對MVCC具體操作的介紹之後,一開始感覺很有道理,但是自己腦補一個實例之後,馬上感覺書中對SELECT操作的介紹存在的疑點,例如,在RR隔離級別下我開啓了一個事務(事務版本號:1024),並且插入了一條id = 5的數據行,可想而知這條數據的行版本號應該是創建它的事務版本號1024,此刻(上一條事務未提交)我新開啓一個事務(事務版本號1025),對全表進行SELECT,按照上面的邏輯當前事務可以查詢到版本小於當前事務版本的數據行,那就是說可以查詢到id = 5的數據。但是,按照RR隔離級別的約定,版本號爲1025的事務並不被允許讀取到這行數據,這就產生了矛盾。說到這裏,大家也都應該能體會到我說的問題所在了。

聽聽官方文檔怎麼說

既然書中所述存在疑問,那我們就去看看MySQL的官方文檔怎麼說:
這裏寫圖片描述
參考以上部分文檔中所述,文中明確指出InnoDB爲每一行數據都添加了三個隱藏字段,而刪除標記有沒有開闢特有的字段並未顯式的說明,只說了在“特殊位置”被標記刪除。也就是說,除了用戶定義的字段以外,還有三個隱式的字段,簡單的結構如下:
這裏寫圖片描述
從圖中可以看出這三個字段的具體作用,其中DB_ROW_ID是行ID,數據表在InnoDB的底層存儲結構爲B+樹,而B+樹需要根據主鍵來生成聚集索引,如果數據表的創建者未定義主鍵,那麼InnoDB將會默認DB_ROW_ID作爲主鍵來生成聚集索引;DB_TRX_ID是該數據行的事務ID,也就是前面《高性能MySQL》中所說的行“創建時間”;DB_ROLL_PTR保存的是一個指針,這個指針指向了該行回滾段中的undo_log。而MVCC就是根據DB_ROLL_PTR、DB_ROLL_PTR這兩個字段(還有一個在“特殊位置”的刪除標記)來構建事務可視版本(快照)的。

什麼是回滾段?

回滾段是一個保存每條數據行之前版本日誌的地方。回滾段中的撤銷日誌分爲插入和更新撤銷日誌,插入撤銷日誌僅僅在事務回滾時需要,事務一提交就可以丟棄,更新撤銷日誌也用於一致性讀取,但是只有在InnoDB沒有分配快照的情況下,纔可以丟棄這些快照,在一致性讀取中可能需要更新撤銷日誌中的信息來構建數據庫行的早期版本。

回滾段如何構造?

當一個事務更新一條記錄時,會將更新後的記錄作爲新的一行插入,將舊的行構建爲undo_log記錄在回滾段中,並將新數據行的回滾字段DB_ROLL_PTR指向這個undo_log
這裏寫圖片描述
當多個事務更新同一條事務時,undo_log會形成鏈式結構
這裏寫圖片描述

瞭解了以上內容,我們開始具體討論InnoDB-MVCC是如何實現多版本控制的。多版本實際上就是不同的事務都有着自己可視的數據版本,不同的事務數據版本是不同的。InnoDB-MVCC通過快照讀的方式構建事務自己的可視版本,簡單來說就是在事務操作之前獲取當前的數據快照,這個快照所“呈現”的數據就是我當前事務的可視版本。那麼快照該如何構建呢?繼續往下看。

READ_VIEW

read_view是MySQL底層實現的一個結構體,是和SQL語句綁定的,在每個SQL語句執行前申請或獲取。可以將其理解爲構造快照的前提或者依據,一個快照所呈現的數據是什麼樣子(版本)的基本依賴於read_view中所存儲的數據。

READ_VIEW底層實現

read_view是MySQL底層使用C++代碼實現的一個結構體,如下圖所示:
這裏寫圖片描述
其中,構建當前可視版本(快照)主要用到的變量有low_limit_id、up_limit_id、trx_ids以及creator_trx_id:

  • low_limit_id:表示創建read_view時,當前事務活躍讀寫鏈表中最大的事務ID
  • up_limit_id:表示創建read_view時,當前事務活躍讀寫鏈表中最小的事務ID
  • trx_ids:創建read_view時,活躍事務鏈表裏所有的事務ID
  • creator_trx_id:當前read_view所屬事務的事務版本號
    什麼是當前事務活躍讀寫鏈表呢?可以將其理解爲一個事務池,事務池中所存儲的是當前所有正在運行(已開啓,但未提交)事務的信息。MySQL將所有當前活躍的事務信息保存在information_schema.innodb_trx表中,如下圖所示:
    這裏寫圖片描述

READ_VIEW的作用

MVCC會根據read_view中所保存的信息來構建當前事務可視版本。

對於小於或者等於RC的隔離級別,事務開啓後,每次執行SQL語句都會申請一個read_view,然後在執行完這個SQL語句後,調用read_view_close_for_mysql將read view從事務中刪除。每次在執行SQL語句之前都會判斷trx->read_view爲空(理論下必爲空),然後重新申請一個read_view(這就是爲什麼RC隔離級別下會產生不可重複讀的原因)。

對於RR隔離級別,當申請一個read_view後,事務未提交不會刪除,整個事務將不再申請新的read_view,保證事務中所使用的read_view都是同一個,從而實現可重複讀的隔離級別。

MVCC-SELECT可見範圍(總結)

瞭解了這麼多,我們再回過頭來總結一下MVCC的SELECT規則。因爲除了上面所提到了部分內容,官方文檔中也沒有很詳細的介紹MVCC的具體操作,我看了很多網上的總結,有人總結了三條,也有人總結了四條,但通過分析以後,本文總結六條供大家參考:
(1):DB_TRX_ID >= view->low_limit_id的記錄不可見。DB_TRX_ID >= view->low_limit_id的記錄必爲當前事務開啓之後開啓的事務更新或插入的,所以不可見;
(2):DB_TRX_ID位於[view->up_limit_id,view->low_limit_id)區間時,如果存在於trx_ids集合中,則不可見。如果DB_TRX_ID存在於這個集合中,說明該記錄的修改或創建者(事務)在當前事務開啓時並未提交,所以不可見;
(3):DB_TRX_ID < view->up_limit_id的記錄可見。DB_TRX_ID < view->up_limit_id,說明該記錄的修改或創建者(事務)在當前事務開啓之前已經提交,所以可見;
(4):DB_TRX_ID = creator_trx_id的記錄可見。DB_TRX_ID = creator_trx_id說明該記錄的修改或創建者(事務)是當前事務,所以可見;
(5):DB_TRX_ID != creator_trx_id的被標記刪除記錄可見。所有被刪除且已提交的事務將被真正刪除(刪除但未提交只是標記刪除),所以不會查詢到,標記刪除的記錄除自身刪除的以外,當前事務可見,DB_TRX_ID = creator_trx_id爲自身刪除所以不可見,其餘皆可見;
(6):以上對於view不可見的記錄,需要通過記錄的DB_ROLL_PTR指針遍歷回滾段中的undo_log構造當前view可見版本數據。不可見的記錄只是說明該記錄的當前版本不可見,但是它之前的某一版本是當前事務可見的,所以應當構建出該數據當前事務的可見版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章