MySQL MVCC機制
什麼是MVCC
MVCC (Multiversion Concurrency Control)
中文全程叫多版本併發控制,是現代數據庫(包括 MySQL
、Oracle
、PostgreSQL
等)引擎實現中常用的處理讀寫衝突的手段,目的在於提高數據庫高併發場景下的吞吐性能。
如此一來不同的事務在併發過程中,SELECT
操作可以不加鎖而是通過 MVCC
機制讀取指定的版本歷史記錄,並通過一些手段保證保證讀取的記錄值符合事務所處的隔離級別,從而解決併發場景下的讀寫衝突。
InnoDB 中的 MVCC
MySQL
中InnoDB
引擎支持MVCC
- 應對高併發事務,
MVCC
比單純的加行鎖更有效, 開銷更小 MVCC
在讀已提交(Read Committed)
和可重複讀(Repeatable Read)
隔離級別下起作用MVCC
既可以基於樂觀鎖又可以基於悲觀鎖來實現
InnoDB MVCC 實現原理
begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第 一個操作InnoDB表的語句,事務才真正啓動,纔會向mysql申請事務ID,mysql內部是嚴格照事務的啓動順序來分配事務ID的事務ID依次遞增
InnoDB
中 MVCC
的實現方式爲:每一行記錄都有兩個隱藏列:DATA_TRX_ID
、DATA_ROLL_PTR
(如果沒有主鍵,則還會多一個隱藏的主鍵列)。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-51oF0K3l-1588519526486)(https://i.loli.net/2020/05/03/zj6l4avrf9VCNsI.png)]
-
DATA_TRX_ID
記錄最近更新這條行記錄的
事務 ID
,大小爲6
個字節 -
DATA_ROLL_PTR
表示指向該行回滾段
(rollback segment)
的指針,大小爲7
個字節,InnoDB
便是通過這個指針找到之前版本的數據。該行記錄上所有舊版本,在undo
中都通過鏈表的形式組織。 -
DB_ROW_ID
行標識(隱藏單調自增
ID
),大小爲6
字節,如果表沒有主鍵,InnoDB
會自動生成一個隱藏主鍵,因此會出現這個列。另外,每條記錄的頭信息(record header
)裏都有一個專門的bit
(deleted_flag
)來表示當前記錄是否已經被刪除。
舉例說明
有一張表:user
字段有id
,name
事務執行表:
#Transaction 100 | #Transaction 200 | #Transaction 300 | #select | RR-ReadView | RC-ReadView |
---|---|---|---|---|---|
begin; | begin; | begin; | begin; | ||
update user set name=‘zxl300’ where id=1; | |||||
commit; | |||||
select name from user where id=1; | [100,200],300 rs:zxl300 | [100,200],300 rs:zxl300 | |||
update user set name=‘zxl1’ where id=1; | |||||
update user set name=‘zxl2’ where id=1; | |||||
select name from user where id=1; | [100,200],300 rs:zxl300 | [100,200],300 rs:zxl300 | |||
commit; | update user set name=‘zxl3’ where id=1; | ||||
update user set name=‘zxl4’ where id=1; | |||||
select name from user where id=1; | [100,200],300 rs:zxl300 | [200],300 rs:zxl2 | |||
commit; |
如何組織 Undo Log 鏈
- 對
user
表id=1
的這行記錄加排他鎖。- 把該行原本的值拷貝到
undo log
中,DB_TRX_ID
和DB_ROLL_PTR
都不動- 修改該行的值這時產生一個新版本,更新
DATA_TRX_ID
爲修改記錄的事務ID
,將DATA_ROLL_PTR
指向剛剛拷貝到undo log
鏈中的舊版本記錄,這樣就能通過DB_ROLL_PTR
找到這條記錄的歷史版本。如果對同一行記錄執行連續的UPDATE
,Undo Log
會組成一個鏈表,遍歷這個鏈表可以看到這條記錄的變遷- 記錄
redo log
,包括undo log
中的修改
那麼
INSERT
和DELETE
會怎麼做呢?其實相比UPDATE
這二者很簡單,INSERT
會產生一條新紀錄,它的DATA_TRX_ID
爲當前插入記錄的事務ID
;DELETE
某條記錄時可看成是一種特殊的UPDATE
,其實是軟刪,真正執行刪除操作會在commit
時,DATA_TRX_ID
則記錄下刪除該記錄的事務ID
。
DB_TRX_ID 只有
insert
/update
/delete
時創建
ReadView 生成規則
當執行查詢SQL
時會生成一致性視圖ReadView
,它由執行查詢時所有未提交的事務ID數組(數組中最小的id爲mid_id
)和已創建的最大事務ID(max_id
)組成,查詢的數據結果需要跟ReadView
做比對從而得到快照結果。
RU 下的 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
。
RC 下的 ReadView 生成
在 RC
隔離級別下,每個 SELECT
語句開始時,都會重新將當前系統中的所有的活躍事務拷貝到一個列表生成 ReadView
。二者的區別就在於生成 ReadView
的時間點不同,一個是事務之後第一個 SELECT
語句開始、一個是事務中每條 SELECT
語句開始。
MVCC 判斷流程
根據ReadView生成規則可以將數據分爲三個區域,小於最小未提交事務Id的稱爲已提交事務大於未提交事務最小ID小於已創建最大事務ID稱爲未提交與已提交事務,大於已創建最大事務ID稱爲未開始事務
- 如果ROW的ID(DB_TRX_ID)落在綠色部分(DB_TRX_ID<mid_id),表示這個版本是已提交的事務生成的,這個數據是可見的。
- 如果ROW的ID(DB_TRX_ID)落在紅色部分(DB_TRX_ID>max_id),表示這個版本是由將來啓動事務生成的,是肯定不可見的。
- 如果ROW的ID(DB_TRX_ID)落在黃色部分(mid_id<=DB_TRX_ID<=max_id),則分爲兩種情況:
- 若
DB_TRX_ID
在未提交的事務ID數組,表示這個版本是由還沒有提交的事務生成的,不可見,當前自己的事物是可見的。 - 若
DB_TRX_ID
不在未提交的事務ID數組,表示這個版本是已提交的事務生成的,可見。
- 若
對於刪除的情況可以認爲是update的特殊情況,會將版本鏈是最新的數據複製一份,然後將
DB_TRX_ID
修改成刪除操作的DB_TRX_ID
,同時在該條記錄的頭信息(record header)裏(deleted flag)標記位寫上ture,來表示當前記錄已被刪除,在查詢時按照上而的規則查詢到對應的記錄如果deleted flag標記位爲ture,意味着記錄已被刪除,則不返回數據。
例RR MVCC 判斷流程
-
user 表中存在一條用戶信息,id:1 name:zxl.
-
首先#Transaction 100,#Transaction 200, #Transaction 300及#select同時開啓事務。
-
#Transaction 300更新name爲zxl300並提交。
-
#select進行查詢操作,首次查詢會創建ReadView,創建ReadView爲[100,200],300,根據MVCC判斷流程進行判斷,當前記錄鏈路DB_TRX_ID爲300,300的區間應該在未提交及已提交範圍,300不在未提交數組範圍,所以本條記錄是可見的,即返回
zxl300
. -
而後#Transaction 100,進行了兩次更新
update user set name='zxl1' where id=1;
,update user set name='zxl2' where id=1;
。 -
#select再次進行查詢
select name from user where id=1;
,因爲事務隔離級別爲RR所以ReadView依然爲[100,200],300,當前記錄鏈路DB_TRX_ID爲 100 ,100的區間應該在未提交及已提交範圍,100在未提交數組範圍,因此本條記錄不可見,根據鏈路繼續查詢,發現還是100,則繼續鏈路查詢發現爲300發現不在未提交範圍內則返回zxl300
。