一、背景
熟悉數據庫隔離級別的人都知道,在RR(可重復讀)隔離級別下,無論何時多次執行相同的SELECT快照讀語句,得到的結果集都是完全一樣的,即便兩次SELECT語句執行期間,其他事務已經改變了該查詢結果並已經提交。
對於這一機制的實現原理,網上常見的一種解釋如下:
####MVCC在MySQL的InnoDB中的實現 在InnoDB中,會在每行數據后添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。 在實際操作中,存儲的並不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下: SELECT時,讀取創建版本號<=當前事務版本號,刪除版本號為空或>當前事務版本號。 INSERT時,保存當前事務版本號為行的創建版本號 DELETE時,保存當前事務版本號為行的刪除版本號 UPDATE時,插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行
上述解釋確實可以讓讀者簡單快速地理解MVCC機制的核心思想,我最開始也以為自己已經完全理解MVCC機制的實現原理了,但是當我試圖利用上述原理去解釋某些特別的實測結果時,卻發現總是難以自圓其說。
二、無法自圓其說的用例
2.1 用例1:為什么看不到尚未提交的記錄
如上圖所示,事務2中,雖然記錄A的創建版本號1小於當前事務版本號2,但是依然無法讀取到記錄A。
2.2 用例2:為什么看不到先啟動事務中插入的記錄
如上圖所示,事務2中,雖然記錄A的創建版本號小於當前事務版本號2,且記錄A已經提交,但是第二次查詢時,事務2依然無法查詢到任何記錄。
2.3 用例3:為什么可以看到后啟動事務中插入的記錄
如上圖所示,事務1中,雖然記錄A的創建版本號大於當前事務版本號1,但是事務1依然可以查詢到記錄A。
2.4 結論
可見,常見的對MVCC版本的實現原理的理解似乎遺漏了某些關鍵邏輯,導致無法解釋很多特殊情況。
下面我們來一起看一下InnoDB中,MVCC機制到底是如何控制記錄的可見性的。
三、遺漏的關鍵控制邏輯
InnoDB RR隔離界別下,MVCC對記錄可見性控制,還有如下關鍵判定邏輯:
1. 事務ID並非在事務begin時就分配,而是在事務首次執行非快照讀操作(SELECT ... FOR UPDATE/IN SHARE MODE、UPDATE、DELETE)時分配。
注: 如果事務中只有快照讀,InnoDB對只有快照讀事務有特殊優化,這類事務不會擁有事務ID,因為它們不會在系統中留下任何修改(甚至連鎖都不會建),所以也沒有留下事務ID的機會。 雖然使用SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID(); 查詢此類事務ID時,會輸出一個很大的事務ID(比如328855902652352),不過這只是MySQL在輸出時臨時隨機分配的一個用於顯示的ID而已。
2. 每個事務首次執行快照讀操作時,會創建一個read_view對象(可以理解為在當前事務中,為數據表建立了一個邏輯快照,read_view對象就是用來控制此邏輯快照的可見范圍的)。事務提交后,其創建的read_view對象將被銷毀。
read_view對象中有三個關鍵字段用於判斷記錄的可見范圍。它們分別是trx_ids、low_limit_id、up_limit_id。 1. read_view->trx_ids:創建該read_view時,記錄正活躍的其他事務的ID集合。事務ID在集合中降序排列,便於二分查找。 2. read_view->low_limit_id:當前活躍事務中的最大事務ID+1(即系統中最近一個尚未分配出去的事務號)。 3. read_view->up_limit_id:當前活躍事務中的最小事務ID。
3. 如果記錄的版本號比自己事務的read_view->up_limit_id小,則該記錄的當前版本一定可見。因為這些版本的內容形成於快照創建之前,且它們的事務也肯定已經commit了。或者如果記錄的版本號等於自己事務的事務ID,則該記錄的當前版本也一定可見,因為該記錄版本就是本事務產生的。
4. 如果記錄的版本號與自己事務的read_view->low_limit_id一樣或比它更大,則該版本的記錄的當前版本一定不可見。因為這些版本的內容形成於快照創建之后。
不可見有如下兩層含義: 1. 如果該記錄是新增或修改后形成的新版本記錄,則對新增和修改行為不可見,即看不到最新的內容; 2. 如果該記錄是標記為已刪除形成的新版本記錄,則對該刪除行為不可見,即可以看到刪除前的內容。
5. 當無法通過4和5快速判斷出記錄的可見性時,則查找該記錄的版本號是否在自己事務的read_view->trx_ids列表中,如果在則該記錄的當前版本不可見,否則該記錄的當前版本可見。
6. 當一條記錄判斷出其當前版本不可見時,通過記錄的DB_ROLL_PTR(undo段指針),嘗試去當前記錄的undo段中提取記錄的上一個版本進行4~6中同樣的可見性判斷,如果可以則該記錄的上一個版本可見。
四、用例的正確解釋
為了更好的檢驗上面新增的知識,對部分用例進行了適當的擴展。
4.1 用例1的正確解釋
4.2 用例2的正確解釋
4.3 用例3的正確解釋
五、總結
要正確理解InnoDB RR隔離級別下MVCC的可見性控制邏輯,需注意補充如下關鍵知識:
1. 事務ID並非事務begin時分配,是延遲到需要分配時才分配的。
2. 事務在首次快照讀時創建快照,並將快照版本的可見范圍控制信息記錄在read_view對象中。