天天看點

InnoDB MVCC 詳解

InnoDB支援MVCC(Multi-Version Concurrency Control), undo日志中儲存了多版本的記錄,undo支援事務復原的同時,也支援資料的一緻性讀。undo日志儲存在復原段中,undo日志的回收由purge操作進行。InnoDB行記錄中儲存了事務相關資訊如事務id,roll_ptr。id用于可見性判斷,roll_ptr用于從undo中回溯曆史版本。一緻性讀會開啟一個ReadView,ReadView包含目前正在執行的事務資訊,通過此ReadView來擷取一緻性的記錄。

MVCC最重要的特點是一緻性讀不加鎖,這樣一緻性讀不會阻塞更新,進而提升了資料庫的并發性能。

2. InnoDB 行格式

InnoDB的行格式如下圖,其中cluster index中的行記錄包含了DB_TRX_ID和DB_ROLL_PTR字段:

InnoDB MVCC 詳解

DB_TRX_ID, 儲存事務id,即trx->id, 用于可見性判斷。

DB_ROLL_PTR, 儲存復原段位址資訊(spaceid,pageno,offset),用于回溯上一個版本。

例如t1表記錄格式如下:

InnoDB MVCC 詳解
注:無主鍵表會自動加一個DB_ROW_ID字段代替PK

3. undo

undo日志儲存在復原段中,復原段在ibdata或單獨的undo tablespace中。

InnoDB MVCC 詳解

undo記錄的主要類型如下,其中TRX_UNDO_INSERT_REC為insert的undo,其他為update和delete的undo。

對于insert和delete,undo中會記錄鍵值,delete操作隻是标記删除(delete mark)記錄。對于update,如果是原地更新,undo中會記錄鍵值和老值。

update如果是通過delete+insert方式進行的,則undo中記錄鍵值,不需記錄老值。其中delete也是标記删除記錄。二級索引的更新總是delete+insert方式進行。具體日志格式參考trx_undo_report_row_operation。

4. ReadView

InnoDB事務都有對應的ReadView,ReadView儲存目前正在執行的事務資訊。ReadView用于判斷可見性。

關于可見性,虛線表示目前讀的時間點,以此劃為三部分:

與虛線時間點相交的,稱為活躍事務,不可見

虛線時間點之前的,已送出事務,可見

虛線時間點之後的,未開啟的事務,不可見

是以可見的事務為T1,T2,T3,T5

在InnoDB中,讀寫事務都會配置設定id(trx_id::id)遞增. trx_sys->rw_trx_ids儲存活躍事務id。InnoDB的中ReadView和可見性判斷如下:

m_ids,目前正在執行的事務id清單。這裡面的事務為活躍事務,不可見;

m_up_limit_id,小于此值的是已送出事務,可見;

m_low_limit_id,大于等于此值的是未開啟的事務,不可見;

trx_id::id   讀寫事務都會從trx_sys->max_trx_id配置設定id,遞增。

上圖ReadView為:

根據可見性規則,可推知:

T1,T2,T3 事務id都小于m_up_limit_id,可見;

T5 不在m_ids裡,可見;

T6 id > m_low_limit_id 不可見;

是以可見的事務為T1,T2,T3,T5。

cluster index上的記錄可見性判斷的相關代碼如下:

在ReadCommit隔離級别下,事務中的每個語句執行前都會配置設定一次ReadView。 在RepeatableRead隔離級别下,隻在事務開始時才配置設定一次ReadView。

5. Purge

Purge操作控制undo日志的回收和真正删除已标記删除的記錄。

5.1 undo回收

undo中儲存老記錄的曆史版本, 當這些曆史版本不再需要時,交由purge清理。

在InnoDB中,trx->no用于儲存事務送出的順序,trx->no在事務送出時從trx_sys->max_trx_id擷取。在ReadView中,m_low_limit_no表示目前已經送出事務的最大trx->no,即小于此值的事務都已送出,且目前ReadView不需要這些事務的undo日志。

trx_sys->mvcc->m_views儲存了目前所有的ReadView,oldest_view為其中最老的ReadView,隻有小于oldest_view->m_low_limit_no的undo才可以purge。下圖中ReadView1的m_low_limit_no為9,ReadView2的m_low_limit_no為12, oldest_view為ReadView1。

是以隻有trx->no小于9的T1和T3的undo可以清理。

事務送出後,會将undo日志資訊(儲存在trx_rseg_t中)加入到隊列purge_queue中,purge_queue是以trx->id排序的最小堆。

purge線程從purge_queue中擷取符合條件(trx->id < oldest_view->m_low_limit_no)的undo并依次purge回收。

事務送出後,仍然可能有标記删除的記錄存在。這些記錄在purge時真正删除。

undo日志應該及時purge,undo日志的堆積不僅會導緻復原段空間的增長,而且delete mark的記錄沒有真正删除,也會影響查詢的效率。

6. Multi-Version

InnoDB多版本資料是通過delete mark的行資料和復原段中的undo資訊組成的。例如下圖中記錄存在三個版本,在Repeatable-Read下select * from t1 查詢傳回的是第一個老的版本(1,1,’a’)。

cluster index的曆史版本在undo日志中或為delete mark的記錄,secondary index的曆史版本是delete mark的記錄。例如t1有三個版本資料。

cluster index的曆史版本在undo日志中或為delete mark的記錄,secondary index的曆史版本是delete mark的記錄。例如t1有三個版本資料:

在cluster index中,最新的版本記錄為T3(1,5,roll_ptr,1,'c')其中5為事務id,資料就在page中;上一個版本為T2(1,3,roll_ptr,1,'b'), 可通過T3(1,5,roll_ptr,1,'c')上roll_ptr指向的undo記錄構造出來;而最老的版本為T1(1,1,roll_ptr,1,'a'), 可通過T2(1,3,roll_ptr,1,'b')上roll_ptr指向的undo記錄構造出來。

在secondary index中,最新的版本記錄為T3('c',1),資料就在目前二級索引page中;上一個版本為T2('b',1),資料也在目前二級索引page中,但打上了delete mark标記;而最老的版本為T1('a',1),資料也在目前二級索引page中,但打上了delete mark标記。

7. 可見性判斷

 前面介紹了資料的多版本,這節介紹如何擷取正确的版本。cluster index和secondary index有不同的擷取方式。

 以上節為例,預設隔離級别為RepeatableRead,select * from t1, 查詢結果為老版本(1,1,’a)。其對應的ReadView為:

首先查詢到最新的記錄(1,1,’c’), 其事務id為5, 大于m_low_limit_id(2)是以不可見;

然後通過roll_ptr建構上一個版本(1,1,’b’), 其事務id為3,大于m_low_limit_id(2)仍然不可見;再通過rool_ptr建構出(1,1,’a’),其事務id為1, 小于m_up_limit_id(2),可見;是以最後傳回(1,1,’a’)。具體代碼參考函數row_sel_build_prev_vers。

7.2 secondary index

二級索引記錄中沒trx_id和roll_ptr字段,但二級索引page中記錄了目前page所涉及事務最大的trx->id,參考page_update_max_trx_id。

判斷二級索引記錄可見性時,用此page的事務id比較,如果page事務id小于目前view的m_up_limit_id則認為此記錄可見,否則需要從cluster index。讀取記錄來判斷可見性,參考lock_sec_rec_cons_read_sess。

例如下圖中select * from t1 force index(i_c3) where c3 >= ‘a’,查詢結果為(1,1,’a’)。我們強制使用二級索引i_c3,查詢會先從二級索引讀取符合條件(c3>=’a’)的記錄再回cluster index擷取完整記錄。

 其對應的ReadView為:

 假設二級索引所在page的最大事務id為5,目前view->m_up_limit_id為5, 先讀取記錄(‘a’,1), 事務id為5(5 = view->m_up_limit_id)不可見,需要回cluster index查找,根據上節依次回溯到可見版本(1,1,’a)。

并且判斷二級索引列值和聚集索引列值一緻(row_sel_sec_rec_is_for_clust_rec),是以可以傳回記錄(1,1,’a’);

接着讀取記錄(‘b’,1),事務id為5不可見(5 = view->m_up_limit_id),需要回cluster index查找,依然回溯到可見版本(1,1,’a’),但此時二級索引列值和聚集索引列值(’a’!=’b’)不一緻, 是以(’b’,1)不符合條件。

再讀取記錄(‘c’,1),事務id為5不可見(5 = view->m_up_limit_id),需要回cluster index查找,依然回溯到可見版本(1,1,’a’),但此時二級索引列值和聚集索引列值(’a’!=’c’)也不一緻, 是以(’c’,1)也不符合條件。

InnoDB MVCC 詳解

是以最終傳回符合條件的記錄為(1,1,’a’)。

我們再看下面這個例子,select * from t1 force index(i_c3) where c3 >= ‘a’,查詢結果為(1,1,’c’)。

InnoDB MVCC 詳解

其對應的ReadView為:

先讀取記錄(‘a’,1), page事務id為5可見, 再判斷(‘a’,1)為del mark記錄,不符合條件;

InnoDB MVCC 詳解

然後讀取記錄(‘b’,1), page事務id為5可見, 再判斷(‘b’,1)為del mark記錄,不符合條件;

InnoDB MVCC 詳解

再讀取記錄(‘c’,1), page事務id為5可見, 且(’c’,1)為非del mark記錄,符合條件。

InnoDB MVCC 詳解

是以最終回表傳回符合條件的記錄為(1,1,’c’)。

8. ICP與MVCC

Index Condition Pushdown (ICP) 并不會受到MVCC的影響。在通過cluster index或sendary index查找記錄時,會優先判斷記錄的可見性。如果記錄可見,則會進行下一步的ICP優化。

例如,從二級索引掃描到記錄不可見時,這時候需要回聚集索引判斷可見性,在這之前會優先判斷條件c3=2是否滿足(row_search_idx_cond_check),

如果滿足條件才會掃描聚集索引,否則忽略此記錄。

9. semi-consistent read

semi-consistent read是建立在InnoDB多版本基礎之上的針對update的優化。semi-consistent read是指在ReadCommitted隔離級别或開啟innodb_locks_unsafe_for_binlog的情況下,當update語句讀取的行正在被其他會話更新時,不直接加鎖等待,而是先讀取此行最近的一個曆史版本,如果此版本符合where條件則重新讀取此行并加鎖,如果不符合where條件則忽略此行。semi-consistent read避免了一些鎖等待。

InnoDB MVCC 詳解
InnoDB MVCC 詳解

而下圖中,而在Read-Committied隔離級别下,會走semi-consistent read,session2的更新成功。session2在執行update t2 set c2=3 where c2=2;時,先讀取到(1,2)這個記錄,發現此記錄已被session1持有鎖,于是讀取最近的一個曆史版本(1,1), 發現此記錄不符合where條件,于是忽略此記錄,結果更新0行傳回。

InnoDB MVCC 詳解

如果session2更新條件變化為update t2 set c2=3 where c2=1;那麼session2将等待。

session2在執行update t2 set c2=3 where c2=1;時,先讀取到(1,2)這個記錄,發現此記錄已被session1持有鎖,于是讀取最近的一個曆史版本(1,1), 發現此記錄符合where條件,于是重新讀此行加鎖等待。

InnoDB MVCC 詳解
具體實作可以參考相關函數try_semi_consistent_read/was_semi_consistent_read

9.2 semi-consistent read存在的問題

然而,semi-consistent read也存在問題,某些情況會破壞事務的串行化(serializability)。可串行化是指session1和session2并發執行完成後,理想情況最終結果應該為以下情況中的一種:

session1先執行完成後session2再執行

session2先執行完成後session1再執行

以下例子semi-consistent read打破了上述規則,session1和session2并發執行完後,結果為

此結果與後面即将介紹的兩種情況的結果都不同。

InnoDB MVCC 詳解

如果session1先執行完成後session2再執行,結果為:

InnoDB MVCC 詳解

而如果session2先執行完成後session1再執行,結果為:

InnoDB MVCC 詳解
在Repeatable-Read隔離級别下不存在上述問題,是以在某些事務性要求比較高的場景建議使用Repeatable-Read隔離級别。

10. 相關BUG

Bug#84958 (https://bugs.mysql.com/bug.php?id=84958)中描述了通過二級索引掃描記錄時需要從聚集索引判斷其可見性的性能問題。