innoDB維護了一個系統版本號,每開始一個事務,系統版本號都會遞增作為這個事務的ID.
Innodb引擎會為每一行添加3個隱藏欄位
| ~ | ~ | | ------------- | ---------------------------------- | | DATA _TRX_ID | 表示產生當前記錄項的事務ID | | DATA_ROLL_PTR | 一個指向此條記錄項的undo信息的指針 | | DELETED_BIT | 用於標識該記錄是否被刪除。 |
增量保存每一次更新操作的修改.每當更新記錄時,直接操作葉節點的數據行(不論有沒有提交),同時會產生一條undo記錄來記錄這次更新的「反操作」. 被修改的數據行會指向這個「反操作」.
值得一提的是,反操作可以有多個,是一個鏈狀結構.一個可能的結構如下:
那麼一個事務內如何根據「反操作鏈表」得到它所可見的數據版本呢?
我們假設有事務 T1、T2,數據D1初始值為0.
T1對D1做一次+5,並提交(這是為了釋放鎖),產生的log如下:
T2對D1做一次-3,產生的log如下:
現在假設有一個事務要讀取T1修改前的版本,則遍歷鏈表並逐個計算,通過2+3-5即可得到.
2+3-5
現在假設有一個事務要讀T2修改前的版本,通過2+3即可得到.
2+3
上面的例子里,你可能會認為事務T1提交以後就把對應的log清除了,事實上並不是這樣, 是否清除的先決條件是這個log是否還被被某個事務依賴.
每當「開始一個事務」[^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可以提前創建.
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
開啟事務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加一條「反操作」記錄,插入的數據會直接插入到葉子頁,此時表中的數據狀態如下
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),反向以後會得到一個空行.
insert
delete
事務D的最終的查詢結果如下
| id | name | gender | DATA _TRX_ID | DATA_ROLL_PTR | DELETED_BIT | | --- | ---- | ------ | ------------ | ------------- | ----------- | | 1 | Nana | female | 1 | snapshot1 | 0 |
我們概括一下上述過程:
此時事務B、D都處於未提交狀態
根據ACID中的「I」,事務D查看不到B的修改、RR級別查下查看不到C已提交的插入^2. 只能查看到事務A提交「數據1」(即舊數據,也稱為快照)
如何理解Read Committed下,每條查詢產生一個Read View?
如果一個事務內只創建一次ReadView,ReadView里存的始終是「創建ReadView那一刻」的舊的信息, 則即使後面有事務提交了也無法感知,只有更新ReadView,刷新"當前活躍的事務"(也即是「trx_ids」)才可能符合可見性的判定從而讀到已提交的數據.
本文只是講解 「undolog的內部結構」、「可見性判斷規則」.事實上還有「undolog的回收機制」也是一個值得思考的問題.