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