MySQL 核心模塊揭秘,你看明白了嗎?
1. 關(guān)于緩存 undo 段
為了提升分配 undo 段的效率,事務(wù)提交過程中,InnoDB 會緩存一些 undo 段。
只要同時滿足兩個條件,insert undo 段或 update undo 段就能被緩存。
條件 1:undo 段中只有一個 undo 頁。
條件 2:這個唯一的 undo 頁中,已經(jīng)使用的的空間必須小于數(shù)據(jù)頁大小的四分之三。以默認(rèn)大小 16K 的 undo 頁為例,undo 頁中已經(jīng)使用的空間必須小于 12K。
如果 insert undo 段滿足緩存條件,它會加入回滾段的 insert_undo_cached 鏈表頭部。
如果 update undo 段滿足緩存條件,它會加入回滾段的 update_undo_cached 鏈表頭部。
2. InnoDB 提交事務(wù)
二階段提交過程中,commit 階段的 flush 子階段,把 prepare 階段及之前產(chǎn)生的 redo 日志都刷盤了,把事務(wù)執(zhí)行過程中產(chǎn)生的 binlog 日志都寫入 binlog 日志文件了。
sync 子階段根據(jù)系統(tǒng)變量 sync_binlog 的值決定是否觸發(fā)操作系統(tǒng)把 binlog 日志刷盤。
前兩個子階段,都只處理了日志,不涉及 InnoDB 的事務(wù)。這兩個階段完成之后,InnoDB 的事務(wù)還沒有提交,事務(wù)還處于準(zhǔn)備提交狀態(tài)(TRX_STATE_PREPARED)。
commit 子階段才會真正提交 InnoDB 的事務(wù),這個階段完成之后,事務(wù)就提交完成了。
commit 子階段提交 InnoDB 的事務(wù),要做的事情有這些:
- 修改 insert undo 段的狀態(tài)。
- 生成事務(wù)提交號,用于 purge 線程判斷是否能清理某些 update undo 日志組中的 undo 日志。
- 修改 update undo 段的狀態(tài)。
- 把 update undo 段中的 undo 日志組加入回滾段的 history list 鏈表。purge 線程會從這個鏈表中獲取需要清理的 update undo 日志組。
- 把事務(wù)狀態(tài)修改為 TRX_STATE_COMMITTED_IN_MEMORY。
- 釋放事務(wù)執(zhí)行過程中 InnoDB 給表或記錄加的鎖。
- 重新初始化事務(wù)對象,以備當(dāng)前線程后續(xù)使用。
2.1 修改 insert undo 段狀態(tài)
如果事務(wù)插入記錄到用戶普通表,InnoDB 會為事務(wù)分配一個 insert undo 段。
如果事務(wù)插入記錄到用戶臨時表,InnoDB 會為事務(wù)分配另一個 insert undo 段。
InnoDB 可能會給事務(wù)分配 0 ~ 2 個 insert undo 段。commit 子階段會修分配給事務(wù)的所有 insert undo 段的狀態(tài)。
如果 insert undo 段滿足緩存條件,它的狀態(tài)會被修改為 TRX_UNDO_CACHED,否則,它的狀態(tài)會被修改為 TRX_UNDO_TO_FREE。
事務(wù)提交完成之后,InnoDB 會根據(jù)狀態(tài)緩存或者釋放 insert undo 段。
2.2 生成事務(wù)提交號
事務(wù)提交號是事務(wù)對象的 no 屬性,通常用 trx->no 表示。
代碼里,對事務(wù)提交號的注釋是 transaction serialization number,直譯成中文應(yīng)該稱為事務(wù)序列號,或者事務(wù)串行號。
因為 trx->no 是在事務(wù)提交時生成的,我們還是把它稱為事務(wù)提交號更容易理解一些。
只有 update undo 段需要事務(wù)提交號。purge 線程清理 update undo 日志時,會根據(jù) update undo 段的 undo 日志組中保存的事務(wù)提交號,決定是否能清理這個 undo 日志組中的 undo 日志。
修改 update undo 段的狀態(tài)之前,InnoDB 會生成事務(wù)提交號,保存到事務(wù)對象的 no 屬性中。
// storage/innobase/trx/trx0trx.cc
static inline bool trx_add_to_serialisation_list(trx_t *trx) {
...
trx->no = trx_sys_allocate_trx_no();
...
}
trx_sys_allocate_trx_no() 調(diào)用 trx_sys_allocate_trx_id_or_no() 生成事務(wù)提交號。
// storage/innobase/include/trx0sys.ic
// 生成事務(wù) ID
inline trx_id_t trx_sys_allocate_trx_id() {
ut_ad(trx_sys_mutex_own());
return trx_sys_allocate_trx_id_or_no();
}
// 生成事務(wù)提交號
inline trx_id_t trx_sys_allocate_trx_no() {
ut_ad(trx_sys_serialisation_mutex_own());
return trx_sys_allocate_trx_id_or_no();
}
從上面的代碼可以看到,生成事務(wù) ID 和事務(wù)提交號調(diào)用的是同一個方法,trx_sys_allocate_trx_id_or_no() 的代碼如下:
// storage/innobase/include/trx0sys.ic
inline trx_id_t trx_sys_allocate_trx_id_or_no() {
...
// trx_sys_allocate_trx_id_or_no() 每次被調(diào)用
// trx_sys->next_trx_id_or_no 加 1
// trx_id 保存的是加 1 之前的值
trx_id_t trx_id = trx_sys->next_trx_id_or_no.fetch_add(1);
...
return trx_id;
}
trx_sys->next_trx_id_or_no 保存的是下一個事務(wù) ID 或事務(wù)提交號,具體是哪個,取決于是生成事務(wù) ID 還是生成事務(wù)提交號先調(diào)用 trx_sys_allocate_trx_id_or_no()。
也就是說,事務(wù) ID 和事務(wù)提交號是同一條流水線上生產(chǎn)出來的。我們以 trx 1 和 trx 2 兩個事務(wù)為例,來說明生成事務(wù) ID 和事務(wù)提交號的流程。
假設(shè)此時 trx_sys->next_trx_id_or_no 的值為 100,trx 1、trx 2 啟動和提交的順序如下:
- trx 1 啟動。
- trx 2 啟動。
- trx 1 提交。
- trx 2 提交。
其于以上假設(shè),生成事務(wù) ID 和事務(wù)提交號的流程如下:
- trx 1 生成事務(wù) ID,得到 100。trx_sys->next_trx_id_or_no 加 1,結(jié)果為 101。
- trx 2 生成事務(wù) ID,得到 101。trx_sys->next_trx_id_or_no 加 1,結(jié)果為 102。
- trx 1 生成事務(wù)提交號,得到 102。trx_sys->next_trx_id_or_no 加 1,結(jié)果為 103。
- trx 2 生成事務(wù)提交號,得到 103。trx_sys->next_trx_id_or_no 加 1,結(jié)果為 104。
從以上流程可以看到,事務(wù) ID 和事務(wù)提交號都來源于 trx_sys->next_trx_id_or_no,相互之間不會重復(fù)。
2.3 修改 update undo 段狀態(tài)
如果事務(wù)更新或刪除了用戶普通表的記錄,InnoDB 會為事務(wù)分配一個 update undo 段。
如果事務(wù)更新或刪除了用戶臨時表的記錄,InnoDB 會為事務(wù)分配另一個 update undo 段。
InnoDB 可能會給事務(wù)分配 0 ~ 2 個 update undo 段。commit 子階段會修改分配給事務(wù)的所有 update undo 段的狀態(tài)。
如果 update undo 段滿足緩存條件,它的狀態(tài)會被修改為 TRX_UNDO_CACHED,否則,它的狀態(tài)會被修改為 TRX_UNDO_TO_PURGE。
2.4 undo 日志組加入 history list
修改完 update undo 段的狀態(tài),update undo 段的 undo 日志組會加入回滾段的 history list 鏈表。purge 線程會從這個鏈表中獲取要清理的 undo 日志組。
前面已經(jīng)生成了事務(wù)提交號,這里會把事務(wù)提交號寫入 undo 日志組的頭信息中。
如果 update undo 段的狀態(tài)為 TRX_UNDO_CACHED,表示這個 undo 段需要緩存起來。它會加入回滾段的 update_undo_cached 鏈表頭部,以備后續(xù)其它事務(wù)需要 update undo 段時,能夠快速分配。
3. InnoDB 提交事務(wù)完成
前面的一系列操作完成之后,InnoDB 提交事務(wù)的操作就完成了。
現(xiàn)在,要把事務(wù)狀態(tài)修改為 TRX_STATE_COMMITTED_IN_MEMORY。
修改之后,新啟動的事務(wù)就能看到該事務(wù)插入或更新的記錄,看不到當(dāng)前事務(wù)刪除的記錄。
接下來,InnoDB 會釋放事務(wù)執(zhí)行過程中加的表鎖、記錄鎖。
釋放鎖之后,還要處理 insert undo 段。
如果 insert undo 段的狀態(tài)為 TRX_UNDO_CACHED,表示這個 undo 段需要緩存起來。它會加入回滾段的 insert_undo_cached 鏈表頭部,以備后續(xù)其它事物需要 insert undo 段時,能夠快速分配。
如果 insert undo 段的狀態(tài)為 TRX_UNDO_TO_FREE,它會被釋放,占用的 undo 頁會還給 undo 表空間。
二階段提交的 flush 子階段,已經(jīng)把 prepare 階段及之前產(chǎn)生的 redo 日志都刷盤了。
commit 子階段,修改 insert undo 段和 update undo 段的狀態(tài),還會產(chǎn)生 redo 日志。
InnoDB 不會主動觸發(fā)操作系統(tǒng)把這些 redo 日志刷盤,而是由操作系統(tǒng)決定什么時候把這些 redo 日志刷盤。
InnoDB 敢這么做,是因為這些 redo 日志對于確定事務(wù)狀態(tài)已經(jīng)不重要了。即使這些 redo 日志刷盤之前,服務(wù)器突然異常關(guān)機,導(dǎo)致 undo 段的狀態(tài)丟失。MySQL 下次啟動時,也能正確的識別到事務(wù)已經(jīng)提交完成了。
4. 重新初始化事務(wù)對象
到這里,InnoDB 提交事務(wù)該做的操作都已經(jīng)做完了。提交事務(wù)完成之后,該做的事也都做了。
對于上一個事務(wù),事務(wù)對象的使命已經(jīng)結(jié)束。這里會把事務(wù)狀態(tài)修改為 TRX_STATE_NOT_STARTED。
事務(wù)對象也會被重新初始化,但是它不會被釋放。也就是說,事務(wù)對象不會回到事務(wù)池中,而是留給當(dāng)前連接后續(xù)啟動新事務(wù)時復(fù)用。
5. 總結(jié)
InnoDB 提交事務(wù),就像我們填完一個表格之后,最后蓋上的那個戳,總體上來說,要干 3 件事。
第 1 件,修改分配給事務(wù)的各 undo 段的狀態(tài)。
如果數(shù)據(jù)庫發(fā)生崩潰,重新啟動后,undo 段的狀態(tài)是影響事務(wù)提交還是回滾的因素之一。
第 2 件,修改事務(wù)對象的狀態(tài)。
如果數(shù)據(jù)據(jù)庫一直運行,不發(fā)生崩潰,就靠事務(wù)對象的狀態(tài)來標(biāo)識事務(wù)是否已提交。
第 3 件,把各 undo 段中的 undo 日志組加入 history list 鏈表。
其它事務(wù)都不再需要使用這些 undo 日志時,后臺 purge 線程會清理這些 undo 日志組中的日志。