• InnoDB中的MVCC
    • 事務ID
    • 數據行的結構
    • UndoLog
    • ReadView
    • 示例
      • 1. Multi-Version的產生過程
      • 2. 判斷可見性的過程
    • 其他
    • 最後

事務ID

innoDB維護了一個系統版本號,每開始一個事務,系統版本號都會遞增作為這個事務的ID.

數據行的結構

Innodb引擎會為每一行添加3個隱藏欄位

| ~ | ~ |
| ------------- | ---------------------------------- |
| DATA _TRX_ID | 表示產生當前記錄項的事務ID |
| DATA_ROLL_PTR | 一個指向此條記錄項的undo信息的指針 |
| DELETED_BIT | 用於標識該記錄是否被刪除。 |

UndoLog

增量保存每一次更新操作的修改.每當更新記錄時,直接操作葉節點的數據行(不論有沒有提交),同時會產生一條undo記錄來記錄這次更新的「反操作」. 被修改的數據行會指向這個「反操作」.

值得一提的是,反操作可以有多個,是一個鏈狀結構.一個可能的結構如下:

那麼一個事務內如何根據「反操作鏈表」得到它所可見的數據版本呢?

我們假設有事務 T1、T2,數據D1初始值為0.

T1對D1做一次+5,並提交(這是為了釋放鎖),產生的log如下:

T2對D1做一次-3,產生的log如下:

現在假設有一個事務要讀取T1修改前的版本,則遍歷鏈表並逐個計算,通過2+3-5即可得到.

現在假設有一個事務要讀T2修改前的版本,通過2+3即可得到.

上面的例子里,你可能會認為事務T1提交以後就把對應的log清除了,事實上並不是這樣, 是否清除的先決條件是這個log是否還被被某個事務依賴.

ReadView

每當「開始一個事務」[^1]或「創建一個行」時(取決於隔離級別),創建一個ReadView.結構如下:

| ~ | ~ |
| -------------- | ----------------------------------------- |
| creator_trx_id | 創建這個ReadView事務id |
| trx_ids | 創建當前ReadView時,所有活躍的事務的id列表 |
| m_trx_ids | `trx_ids`列表長度 |
| up_limit_id | 創建當前ReadView時,活躍事務的最小id |
| low_limit_id | 創建當前ReadView時,活躍事務的最大id |

[^1]: 準確的說,是開始事務後,執行了Select後才建立ReadView,使用START TRANSACTION WITH CONSISTENT SNAPSHOT可以提前創建.

判斷可見性規則如下:

IsVisible(trx_id)
//該數據行是否是當前事務創建, 則可見
if (trx_id == creator_trx_id)
return true;
//創建該數據行的事務在 「當前事務的ReadView創建之前」已經提交, 則可見
else if (trx_id < up_limit_id)
return true;
// 創建該數據行的事務在 「當前ReadView創建之後開始(不論有沒有提交)」,則不可見.
else if (trx_id > low_limit_id)
return false;
// 創建該數據行的事務在 「當前ReadView創建時未提交」,則不可見.
else if (trx_id is in m_ids)
return false
// 提交該數據的事務 在「當前事務之後創建」, 且「當前事務的ReadView創建之前提交」
else
return true;

以上偽代碼出處?? 知乎用戶「郭華」的回答

示例

以下面的表為例子

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`gender` enum(male,female) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB

Undolog的產生過程

開啟事務A,插入一條數據,並提交

begin;
insert into user values(null,Nana,female)
commit;

假設事務A的id為「1」,則表的的狀態如下

這裡忽略了事務A產生的undolog,因為insert的undolog會在提交以後被刪除.

開啟事務B,修改一條數據,並假設事務B的id為「2」.

begin;
update user set name=Nana2 where id=1;

此時會向undolog加一條「反操作」記錄用於記錄舊版本的數據,而事務B的修改將直接落地到數據行,並指向剛才產生的快照,則此時表的的狀態如下

事務C開始,執行插入,假設分配的事務id為「3」

begin;
insert into user values(2,Nujabes,male);

此時會向undolog加一條「反操作」記錄,插入的數據會直接插入到葉子頁,此時表中的數據狀態如下

判斷可見性的過程

事務D開始,執行查詢,假設分配的事務id為「4」

begin;
select * from user;

此時會為事務D創建一個「ReadView」

| creator_trx_id | trx_ids | m_trx_ids | up_limit_id | low_limit_id |
| -------------- | ------- | --------- | ----------- | ------------ |
| 4 | [2,3] | 2 | 2 | 3 |

讀者可對照前文對「ReadView」的介紹來理解上面的表格內容,這裡不再贅述.

為了測試隔離性的效果,我們提交一下事務C,此時插入的undolog將被刪除,表狀態如下:

接下來就可以開始遍歷「數據行」和「undolog」了,並逐一判斷可見性,具體過程如下:

「第一行」是不可見的,判斷過程如下

//根據現在的數據狀態,第一行的trx_id為2
trx_id = 2
//該數據行是否是當前事務創建?
if (trx_id == creator_trx_id) //2 == 4 flase
return true;
//該數據行是否在當前ReadView創建之前已經提交?
else if (trx_id < up_limit_id) //2 < 2 false
return true;
//該數據行是否是否在當前ReadView創建之後產生的?
else if (trx_id > low_limit_id) //2 > 3 false
return false;
//該數據行是否在當前ReadView創建前「產生但未提交」或者之後 「產生(不管有沒有提交)」?
else if (trx_id is in m_ids) // 2 in [2,3] true
return false // ? 該數據不可見
// 省略後面不會執行的代碼

「第一行指向的undolog記錄」是可見的,判斷過程如下

//undolog記錄的trx_id為1
trx_id = 1
//該數據行是否是當前事務創建?
if (trx_id == creator_trx_id) //1 == 4 flase
return true;
//該數據行是否在當前ReadView創建之前已經提交?
else if (trx_id < up_limit_id) //1 < 2 true
return true; //  ? 該數據可見
// 省略後面不會執行的代碼

執行反操作後得到的數據,將放入「查詢結果集」

「第二行」是不可見的,具體過程如下

//根據現在的數據狀態,第一行的trx_id為3
trx_id = 3
//該數據行是否是當前事務創建?
if (trx_id == creator_trx_id) //3 == 4 flase
return true;
//該數據行是否在當前ReadView創建之前已經提交?
else if (trx_id < up_limit_id) //3 < 2 false
return true;
//該數據行是否是否在當前ReadView創建之後產生的?
else if (trx_id > low_limit_id) //3 > 3 false
return false;
//該數據行是否在當前ReadView創建前「產生但未提交」或者之後 「產生(不管有沒有提交)」?
else if (trx_id is in m_ids) // 3 in [2,3] true
return false // ? 該數據不可見
// 省略後面不會執行的代碼

第二行指向的undolog」是不可見的,因為是insert的反向操作(即delete),反向以後會得到一個空行.

事務D的最終的查詢結果如下

| id | name | gender | DATA _TRX_ID | DATA_ROLL_PTR | DELETED_BIT |
| --- | ---- | ------ | ------------ | ------------- | ----------- |
| 1 | Nana | female | 1 | snapshot1 | 0 |

我們概括一下上述過程:

  1. 事務A插入「數據1」並提交
  2. 事務B修改「數據1」
  3. 事務C插入數據2
  4. 事務D開始
  5. 事務C提交

此時事務B、D都處於未提交狀態

根據ACID中的「I」,事務D查看不到B的修改、RR級別查下查看不到C已提交的插入^2. 只能查看到事務A提交「數據1」(即舊數據,也稱為快照)

其他

如何理解Read Committed下,每條查詢產生一個Read View?

如果一個事務內只創建一次ReadView,ReadView里存的始終是「創建ReadView那一刻」的舊的信息, 則即使後面有事務提交了也無法感知,只有更新ReadView,刷新"當前活躍的事務"(也即是「trx_ids」)才可能符合可見性的判定從而讀到已提交的數據.

最後

本文只是講解 「undolog的內部結構」、「可見性判斷規則」.事實上還有「undolog的回收機制」也是一個值得思考的問題.


推薦閱讀:
相关文章