聊聊 MySQL 事務(wù)二階段提交
回想當(dāng)年,高并發(fā)還沒(méi)有這么普遍,分布式也沒(méi)有這么流行。
初次接觸二階段提交,源于想以事務(wù)的方式實(shí)現(xiàn)對(duì) MongoDB 中多個(gè)集合數(shù)據(jù)的修改,而 MongoDB 本身不支持事務(wù),官方推薦的方案就是使用二階段提交。
MySQL 和事務(wù)早已成為工作中不可或缺的一部分,隨著分布式的流行,二階段提交出現(xiàn)在視野中的次數(shù)也越來(lái)越多。
然而,MySQL、事務(wù)、二階段提交這 3 個(gè)名詞組合在一起成為一個(gè)整體,從第一次接觸到現(xiàn)在也不過(guò)一年時(shí)間。
第一次接觸到 MySQL 事務(wù)二階段提交這個(gè)概念時(shí),心里還有點(diǎn)小激動(dòng)。
因?yàn)楫?dāng)年研究 MongoDB 二階段提交,其實(shí)是沒(méi)有弄明白的。
沒(méi)想到,多年以后,在 MySQL 中發(fā)現(xiàn)了二階段提交的身影,心頭似乎涌現(xiàn)出了那種感覺(jué):眾里尋 TA 千百度,驀然回首,那人卻在燈火闌珊處。
本文我們就一起來(lái)看看 MySQL 事務(wù)是怎么實(shí)現(xiàn)二階段提交的。
本文內(nèi)容基于 MySQL 8.0.29 源碼。
正文
1、什么是二階段提交?
二階段提交是一種用于保證分布式事務(wù)原子性的協(xié)議。
二階段提交的實(shí)現(xiàn)過(guò)程,有 2 個(gè)角色參與其中:
資源管理器,Resource Manager,負(fù)責(zé)管理一部分資源。對(duì)于數(shù)據(jù)庫(kù)來(lái)說(shuō),這里的資源指的就是數(shù)據(jù)。
如果把分布式事務(wù)看成是一個(gè)整體,每個(gè)資源管理器會(huì)負(fù)責(zé)其中的一部分,也就是分布式事務(wù)的一個(gè)本地事務(wù)。
資源管理器在分布式事務(wù)中的角色就是干活的,所以,我們可以稱(chēng)它為執(zhí)行器。
事務(wù)管理器,Transaction Manager,負(fù)責(zé)管理分布式事務(wù),協(xié)調(diào)事務(wù)的提交、回滾、以及崩潰恢復(fù)。
事務(wù)管理器在分布式事務(wù)中,就是那個(gè)總攬全局、指揮資源管理器干活的角色,所以,我們可以稱(chēng)它為協(xié)調(diào)器。
二階段提交,顧名思義,會(huì)包含 2 個(gè)階段:
prepare 階段,協(xié)調(diào)器會(huì)詢(xún)問(wèn)所有執(zhí)行器,是否可以提交事務(wù)。
此時(shí),各個(gè)本地事務(wù)實(shí)際上已經(jīng)執(zhí)行完成,數(shù)據(jù)寫(xiě)入已經(jīng)成功,就差提交這最后一哆嗦了。
如果有任何一個(gè)執(zhí)行器因?yàn)樗鶊?zhí)行的本地事務(wù)有問(wèn)題不能提交,分布式事務(wù)就不能提交,協(xié)調(diào)器會(huì)通知所有執(zhí)行器進(jìn)行回滾操作。
如果每一個(gè)執(zhí)行器都回復(fù)協(xié)調(diào)器可以提交,分布式事務(wù)就會(huì)進(jìn)入下一個(gè)階段,也就是 commit 階段。
commit 階段,協(xié)調(diào)器會(huì)通知執(zhí)行器進(jìn)行提交操作。
執(zhí)行器收到提交通知之后,各自提交自己的本地事務(wù)。
所有執(zhí)行器都提交完成之后,二階段提交就結(jié)束了,分布式事務(wù)也就執(zhí)行完成了。
以上只介紹了二階段提交的正常流程,實(shí)際上二階段提交的復(fù)雜之處在于異常流程處理,對(duì)二階段提交完整流程感興趣的小伙伴,可以自行查找相關(guān)資料。
2、MySQL 二階段提交場(chǎng)景
在 MySQL 中,二階段提交有 4 種使用場(chǎng)景:
場(chǎng)景 1,外部 XA 事務(wù),數(shù)據(jù)庫(kù)中間件、應(yīng)用程序作為協(xié)調(diào)器,MySQL 數(shù)據(jù)庫(kù)實(shí)例作為執(zhí)行器。
XA 事務(wù)也就是分布式事務(wù)。其它支持分布式事務(wù)的數(shù)據(jù)庫(kù)實(shí)例,如 Oracle、SQL Server,也可以和 MySQL 一起作為執(zhí)行器。
這種場(chǎng)景下,MySQL 通過(guò)以下 XA 系列命令來(lái)實(shí)現(xiàn)二階段提交:
- XA START xid,開(kāi)啟分布式事務(wù)。
- XA END xid,標(biāo)識(shí)分布式事務(wù)中的 SQL 都已經(jīng)執(zhí)行完成。
- XA PREPARE xid,執(zhí)行分布式事務(wù)提交的 prepare 階段。
- XA COMMIT xid,執(zhí)行分布式事務(wù)提交的 commit 階段。
- XA ROLLBACK xid,回滾分布式事務(wù)。
以上命令具體怎么使用,可以參照官方文檔的 XA Transactions 小節(jié),鏈接:https://dev.mysql.com/doc/refman/8.0/en/xa.html
場(chǎng)景 2,單個(gè) MySQL 實(shí)例的內(nèi)部 XA 事務(wù),沒(méi)有開(kāi)啟 binlog 日志,SQL 語(yǔ)句涉及多個(gè)支持事務(wù)的存儲(chǔ)引擎。
TC_LOG_MMAP 類(lèi)對(duì)象作為協(xié)調(diào)器,多個(gè)支持事務(wù)的存儲(chǔ)引擎作為執(zhí)行器。
TC_LOG_MMAP 會(huì)打開(kāi)一個(gè)名為 tc.log 的磁盤(pán)文件,并通過(guò) MMAP 映射到內(nèi)存中,用于記錄分布式事務(wù)的 xid。
場(chǎng)景 3,單個(gè) MySQL 實(shí)例的內(nèi)部 XA 事務(wù),沒(méi)有開(kāi)啟 binlog 日志,SQL 語(yǔ)句只涉及 1 個(gè)支持事務(wù)的存儲(chǔ)引擎。
這種場(chǎng)景下,原本是不需要二階段提交的,但是為了統(tǒng)一,還是會(huì)以二階段提交的結(jié)構(gòu)進(jìn)行提交操作。
TC_LOG_DUMMY 類(lèi)對(duì)象作為協(xié)調(diào)器,不記錄 xid,存儲(chǔ)引擎作為執(zhí)行器。
從 DUMMY 可以看出,TC_LOG_DUMMY 是個(gè)偽裝的協(xié)調(diào)器。
場(chǎng)景 4,單個(gè) MySQL 實(shí)例的內(nèi)部 XA 事務(wù),開(kāi)啟了 binlog 日志,SQL 語(yǔ)句涉及 1 個(gè)或多個(gè)支持事務(wù)的存儲(chǔ)引擎。
MYSQL_BIN_LOG 類(lèi)對(duì)象作為協(xié)調(diào)器,分布式事務(wù)的 xid 記錄在 binlog 日志文件中。binlog 日志和存儲(chǔ)引擎作為執(zhí)行器。
binlog 日志和存儲(chǔ)引擎都是獨(dú)立單元,為了保證多個(gè)存儲(chǔ)引擎之間、存儲(chǔ)引擎和 binlog 日志之間的數(shù)據(jù)一致性,在事務(wù)提交時(shí),這些操作要么都提交,要么都回滾,需要借助 XA 事務(wù)實(shí)現(xiàn)。
InnoDB 是 MySQL 最常用的存儲(chǔ)引擎,為了支持主從架構(gòu),binlog 日志也是必須要開(kāi)啟的,這是 MySQL 最常使用的場(chǎng)景。
接下來(lái)我們就以 InnoDB 存儲(chǔ)引擎 + binlog 日志為例,來(lái)介紹 MySQL 內(nèi)部 XA 事務(wù)的二階段提交過(guò)程
3、prepare 階段
來(lái)到 prepare 階段之前,InnoDB 對(duì)表中數(shù)據(jù)的寫(xiě)操作都已經(jīng)完成,就差提交或者回滾這最后一哆嗦了。
prepare 階段,binlog 日志和 InnoDB 主要干的事情有這些:
prepare 階段,binlog 日志沒(méi)有什么需要做的,InnoDB 主要做的事情就是修改事務(wù)和 undo 段的狀態(tài),以及記錄 xid(分布式事務(wù)的 ID)。
InnoDB 會(huì)把內(nèi)存中事務(wù)對(duì)象的狀態(tài)修改為 TRX_STATE_PREPARED,把事務(wù)對(duì)應(yīng) undo 段在內(nèi)存中的對(duì)象狀態(tài)修改為 TRX_UNDO_PREPARED。
修改完內(nèi)存中各對(duì)象的狀態(tài),還不算完事,還要把事務(wù)對(duì)應(yīng) undo 段的段頭頁(yè)中 Undo Segment Header 的 TRX_UNDO_STATE 字段值修改為 TRX_UNDO_PREPARED。
然后,把 xid 信息寫(xiě)入當(dāng)前事務(wù)對(duì)應(yīng)日志組的 Undo Log Header 中的 xid 區(qū)域。
修改 TRX_UNDO_STATE 字段值、寫(xiě) xid,這兩個(gè)操作都要修改 undo 頁(yè),修改 undo 頁(yè)之前會(huì)先記錄 Redo 日志。
4、commit 階段
(1)commit 階段整體介紹
到了 commit 階段,一個(gè)事務(wù)就已經(jīng)接近尾聲了。
寫(xiě)操作(包括增、刪、改)已經(jīng)完成,內(nèi)存中的事務(wù)狀態(tài)已經(jīng)修改,undo 段的狀態(tài)也已經(jīng)修改,xid 信息也已經(jīng)寫(xiě)入 Undo Log Header,prepare 階段產(chǎn)生的 Redo 日志已經(jīng)寫(xiě)入到 Redo 日志文件。
由于 log_flusher 線(xiàn)程會(huì)每秒進(jìn)行刷盤(pán)操作,此時(shí),事務(wù)產(chǎn)生的 Redo 日志有可能已經(jīng)刷新到磁盤(pán)了,也有可能還停留在 Redo 日志文件的操作系統(tǒng)緩沖區(qū)中。
剩余的收尾工作,包括:
- Redo 日志刷盤(pán)。
- 事務(wù)的 binlog 日志從臨時(shí)存放點(diǎn)拷貝到 binlog 日志文件。
- binlog 日志文件刷盤(pán)。
- InnoDB 事務(wù)提交。
為了保證主從數(shù)據(jù)一致性,同一個(gè)事務(wù)中,上面列出的收尾工作必須串行執(zhí)行。
Redo & binlog 日志刷盤(pán)都涉及到磁盤(pán) IO,如果每提交一個(gè)事務(wù),都把該事務(wù)中的 Redo 日志、binlog 日志刷盤(pán),會(huì)涉及到很多小數(shù)據(jù)量的 IO 操作,頻繁的小數(shù)量 IO 操作非常消耗磁盤(pán)的讀寫(xiě)性能。
為了提升磁盤(pán) IO 效率,從而提高事務(wù)的提交效率,MySQL 從 5.6 開(kāi)始引入了 binlog 日志組提交功能,5.7 中把原本在 prepare 階段進(jìn)行的 Redo 日志刷盤(pán)操作遷移到了 commit 階段。
binlog 日志組提交有何神奇之處,怎么就能提升磁盤(pán) IO 效率呢?
引入 binlog 日志組提交功能之后,commit 階段細(xì)分為 3 個(gè)子階段。
對(duì)于每個(gè)子階段,都可以有多個(gè)事務(wù)處于該子階段,寫(xiě)日志 & 刷盤(pán)操作可以合并:
flush 子階段,Redo 日志可以一起刷盤(pán),binlog 日志不需要加鎖就可以一起寫(xiě)入 binlog 日志文件。
sync 子階段,binlog 日志可以一起刷盤(pán)。
commit 子階段,Redo 日志可以一起刷盤(pán)。
通過(guò)合并 Redo 日志刷盤(pán)操作、合并 binlog 日志寫(xiě)入日志文件操作、合并 binlog 日志刷盤(pán)操作,把小數(shù)據(jù)量多次 IO 變?yōu)榇髷?shù)據(jù)量更少次數(shù) IO,可以提升磁盤(pán) IO 效率。
類(lèi)比一下生活中的場(chǎng)景:這就相當(dāng)于每個(gè)人從自己開(kāi)車(chē)上下班,都改為坐公交或地鐵上下班,路上的車(chē)減少了,通行效率大大提升,就不堵車(chē)了。
既然要合并 Redo、binlog 日志的寫(xiě)入、刷盤(pán)操作,那必須有一個(gè)管事的來(lái)負(fù)責(zé)協(xié)調(diào)這些操作。
如果引入一個(gè)單獨(dú)的協(xié)調(diào)線(xiàn)程,會(huì)增加額外開(kāi)銷(xiāo)。
MySQL 的解決方案是把處于同一個(gè)子階段的事務(wù)線(xiàn)程分為 2 種角色:
- leader 線(xiàn)程,第 1 個(gè)進(jìn)入某個(gè)子階段的事務(wù)線(xiàn)程,就是該子階段當(dāng)前分組的 leader 線(xiàn)程。
- follower 線(xiàn)程,第 2 個(gè)及以后進(jìn)入某個(gè)子階段的事務(wù)線(xiàn)程,都是該子階段當(dāng)前分組的 follower 線(xiàn)程。
事務(wù)線(xiàn)程指的是事務(wù)所在的那個(gè)線(xiàn)程,我們可以把事務(wù)線(xiàn)程看成事務(wù)的容器,一個(gè)線(xiàn)程執(zhí)行一個(gè)事務(wù)。
leader 線(xiàn)程管事的方式,并不是指揮 follower 線(xiàn)程干活,而是自己幫 follower 線(xiàn)程把活都干了。
commit 細(xì)分為 3 個(gè)子階段之后,每個(gè)子階段會(huì)有一個(gè)隊(duì)列用于記錄哪些事務(wù)線(xiàn)程處于該子階段。
為了保證先進(jìn)入 flush 子階段的事務(wù)線(xiàn)程一定先進(jìn)入 sync 子階段,先進(jìn)入 sync 子階段的事務(wù)線(xiàn)程一定先進(jìn)入 commit 子階段,每個(gè)子階段都會(huì)持有一把互斥鎖。
接下來(lái),我們一起來(lái)看看這 3 個(gè)子階段具體都干了什么事情。
(2)flush 子階段
flush 子階段,第 1 個(gè)進(jìn)入 flush 隊(duì)列的事務(wù)線(xiàn)程,會(huì)成為 leader 線(xiàn)程。第 2 個(gè)及以后進(jìn)入 flush 隊(duì)列的事務(wù)線(xiàn)程,會(huì)成為 follower 線(xiàn)程。
follower 線(xiàn)程會(huì)進(jìn)入等待狀態(tài),直到收到 leader 線(xiàn)程從 commit 子階段發(fā)來(lái)的通知,才會(huì)醒來(lái)繼續(xù)執(zhí)行后續(xù)操作。
leader 線(xiàn)程會(huì)獲取一把互斥鎖,保證同一時(shí)間 flush 子階段只有一個(gè) leader 線(xiàn)程。
互斥鎖保存到 MYSQL_BIN_LOG 類(lèi)的 Lock_log 屬性中,我們就叫它 Lock_log 互斥鎖好了。
flush 子階段 leader 線(xiàn)程的主要工作流程如下:
第 1 步,清空 flush 隊(duì)列。
清空之前,會(huì)先鎖住 flush 隊(duì)列,在這之前進(jìn)入 flush 隊(duì)列的所有事務(wù)線(xiàn)程就成為了一組。
鎖住之后,會(huì)清空 flush 隊(duì)列。
清空之后,進(jìn)入 flush 隊(duì)列的事務(wù)線(xiàn)程就屬于下一組了,在這之后第 1 個(gè)進(jìn)入 flush 隊(duì)列的事務(wù)線(xiàn)程會(huì)成為下一組的 leader 線(xiàn)程。
有一點(diǎn)需要注意,當(dāng)前組的 leader 線(xiàn)程持有的 Lock_log 鎖要等到 sync 階段才會(huì)釋放。
如果下一組的 leader 線(xiàn)程在當(dāng)前組的 leader 線(xiàn)程釋放 Lock_log 鎖之前就進(jìn)入 flush 隊(duì)列了,下一組的 leader 線(xiàn)程會(huì)阻塞,直到當(dāng)前組的 leader 線(xiàn)程釋放 Lock_log 鎖。
第 2 步,執(zhí)行 Redo 日志刷盤(pán)操作,把 InnoDB 產(chǎn)生的 Redo 日志都刷新到磁盤(pán)。
第 3 步,遍歷 leader 線(xiàn)程帶領(lǐng)的一組 follower 線(xiàn)程,把 follower 線(xiàn)程中事務(wù)產(chǎn)生的 binlog 日志都寫(xiě)入到 binlog 日志文件。
每個(gè)事務(wù)在執(zhí)行過(guò)程中產(chǎn)生的 binlog 日志都會(huì)先寫(xiě)入事務(wù)線(xiàn)程中專(zhuān)門(mén)用于存放該事務(wù) binlog 日志的磁盤(pán)臨時(shí)文件,這是事務(wù)線(xiàn)程中 binlog 日志的臨時(shí)存放點(diǎn)。
binlog 日志磁盤(pán)臨時(shí)文件有一個(gè) IO_CACHE(內(nèi)存緩沖區(qū)),默認(rèn)大小為 32K。
binlog 日志會(huì)先寫(xiě)入 IO_CACHE,寫(xiě)滿(mǎn)之后再把 IO_CACHE 中的日志寫(xiě)入磁盤(pán)臨時(shí)文件,然后清空 IO_CACHE,供后續(xù)的 binlog 日志寫(xiě)入。
等到二階段提交的 flush 子階段,才會(huì)按照事務(wù)提交的順序,把每個(gè)事務(wù)產(chǎn)生的 binlog 日志從臨時(shí)存放點(diǎn)拷貝到 binlog 日志文件中。
binlog 日志文件也有一個(gè) IO_CACHE,大小為 8K。
binlog 日志從臨時(shí)存放點(diǎn)拷貝到 binlog 日志文件,也是先寫(xiě)入 IO_CACHE,寫(xiě)滿(mǎn)之后再把 IO_CACHE 中的日志寫(xiě)入 binlog 日志文件,然后清空 IO_CACHE,供后續(xù) binlog 日志復(fù)用。
在第 1 步中,已經(jīng)早早的把 flush 隊(duì)列給清空了,還怎么遍歷 leader 線(xiàn)程帶領(lǐng)的一組 follower 線(xiàn)程呢?
別急,leader 線(xiàn)程既然作為管事的,它自然得知道它這一組中都有哪些 follower 線(xiàn)程。
每個(gè)線(xiàn)程對(duì)象(thd)中,都會(huì)有個(gè) next_to_commit 屬性,指向緊隨其后加入到 flush 隊(duì)列的線(xiàn)程。
只要知道 leader 線(xiàn)程,根據(jù)每個(gè)線(xiàn)程的 next_to_commit 屬性,就可以順藤摸瓜找到 leader 線(xiàn)程帶領(lǐng)的一組 follower 線(xiàn)程。
第 4 步,把 binlog 日志文件 IO_CACHE 中最后剩下的日志拷貝到 binlog 日志文件。
binlog 日志從臨時(shí)存放點(diǎn)拷貝到 binlog 日志文件的過(guò)程中,得先寫(xiě)入 IO_CACHE,寫(xiě)滿(mǎn)之后,才會(huì)把 IO_CACHE 中的日志拷貝到 binlog 日志文件。
第 3 步執(zhí)行完成之后,binlog 日志文件的 IO_CACHE 可能沒(méi)有寫(xiě)滿(mǎn),其中的日志也就不會(huì)被拷貝到 binlog 日志文件。
所以,第 4 步的存在就是為了把 binlog 日志文件 IO_CACHE 中最后剩下的不足 8K 的日志拷貝到 binlog 日志文件。
flush 子階段把 binlog 日志從臨時(shí)存放點(diǎn)拷貝到 binlog 日志文件,并沒(méi)有刷新到磁盤(pán),只是寫(xiě)入到 binlog 日志文件的操作系統(tǒng)緩沖區(qū)。
(3)sync 子階段
sync 子階段也有一個(gè)隊(duì)列,是 sync 隊(duì)列。
第 1 個(gè)進(jìn)入 sync 隊(duì)列的事務(wù)線(xiàn)程是 sync 子階段的 leader 線(xiàn)程。
第 2 個(gè)及以后進(jìn)入 sync 隊(duì)列的事務(wù)線(xiàn)程是 sync 子階段的 follower 線(xiàn)程。
flush 子階段完成之后,它的 leader 線(xiàn)程會(huì)進(jìn)入 sync 子階段。
flush 子階段的 leader 線(xiàn)程來(lái)到 sync 子階段之后,它會(huì)先加入 sync 隊(duì)列,然后它的 follower 線(xiàn)程也會(huì)逐個(gè)加入 sync 隊(duì)列。
flush 子階段的 leader 線(xiàn)程和它的 follower 線(xiàn)程都加入到 sync 隊(duì)列之后,leader 線(xiàn)程會(huì)釋放它持有的 Lock_log 互斥鎖。
如果 flush 子階段的 leader 線(xiàn)程加入 sync 隊(duì)列之前,sync 隊(duì)列是空的,那么它又會(huì)成為 sync 子階段的 leader 線(xiàn)程,否則,它和它的所有 follower 線(xiàn)程都會(huì)成為 sync 子階段的 follower 線(xiàn)程。
在 sync 子階段,依然是由 leader 線(xiàn)程完成各項(xiàng)工作。
follower 線(xiàn)程依然處于等待狀態(tài),直到收到 leader 線(xiàn)程從 commit 子階段發(fā)來(lái)的通知,才會(huì)退出等待狀態(tài),執(zhí)行后續(xù)操作。
leader 線(xiàn)程開(kāi)展工作之前,會(huì)先獲取 Lock_sync 互斥鎖,保證同一時(shí)間 sync 子階段只有一個(gè) leader 線(xiàn)程。
sync 子階段 leader 線(xiàn)程的主要工作流程如下:
第 1 步,等待更多事務(wù)線(xiàn)程進(jìn)入 sync 子階段。
只有符合一定條件時(shí),leader 線(xiàn)程才會(huì)進(jìn)入等待過(guò)程。
介紹要符合什么條件之前,我們先來(lái)看看 leader 線(xiàn)程為什么要有這個(gè)等待過(guò)程?
前面介紹過(guò),binlog 日志組提交就是為了把多個(gè)事務(wù)線(xiàn)程攢到一起,然后再把這些事務(wù)產(chǎn)生的 Redo 日志、binlog 日志一起刷盤(pán),從而提升磁盤(pán)的 IO 效率。
leader 線(xiàn)程的等待過(guò)程,依然是為了把更多事務(wù)線(xiàn)程攢到一起,從而積攢更多 binlog 日志一起刷盤(pán)。
如果沒(méi)有這個(gè)等待過(guò)程,第 1 個(gè)事務(wù)線(xiàn)程進(jìn)入 sync 隊(duì)列成為 leader 線(xiàn)程之后,它可不管有沒(méi)有其它事務(wù)線(xiàn)程加入 sync 隊(duì)列,就會(huì)馬不停蹄的執(zhí)行后面的流程。
數(shù)據(jù)庫(kù)繁忙的時(shí)候,leader 線(xiàn)程開(kāi)始執(zhí)行后續(xù)流程之前,可能就有很多其它事務(wù)線(xiàn)程加入 sync 隊(duì)列成為它的 follower 線(xiàn)程。
這種情況下,leader 線(xiàn)程有很多 follower 線(xiàn)程,它把這些 follower 線(xiàn)程的 binlog 日志一起刷盤(pán),能夠提升磁盤(pán) IO 效率。
數(shù)據(jù)庫(kù)不那么忙的時(shí)候,leader 線(xiàn)程開(kāi)始執(zhí)行后續(xù)流程之前,可能沒(méi)有或者只有很少的事務(wù)線(xiàn)程加入 sync 隊(duì)列成為它的 follower 線(xiàn)程。
這種情況下,leader 線(xiàn)程還是只能把少量的 binlog 日志一起刷盤(pán),binlog 日志組提交功能提升磁盤(pán) IO 效率就不那么明顯了。
為了在數(shù)據(jù)庫(kù)不那么忙的時(shí)候,也能盡量提升 binlog 日志組提交的效率,引入了 leader 線(xiàn)程的有條件等待過(guò)程,這個(gè)條件由系統(tǒng)變量 sync_binlog 控制。
sync_binlog 表示 binlog 日志刷盤(pán)頻率。
sync_binlog = 0,MySQL 不會(huì)主動(dòng)發(fā)起 binlog 日志刷盤(pán)操作。
只需要把 binlog 日志寫(xiě)入 binlog 日志文件的操作系統(tǒng)緩沖區(qū),由操作系統(tǒng)決定什么時(shí)候執(zhí)行刷盤(pán)操作。
sync_binlog = 1,sync 子階段每一組的 leader 線(xiàn)程都會(huì)觸發(fā)刷盤(pán)操作。
這意味著每個(gè)事務(wù)只要提交成功了,binlog 日志也一定刷新到磁盤(pán)了。
sync_binlog = 1 就是著名的雙 1 設(shè)置的其中一個(gè) 1。
sync_binlog = N,sync 子階段每 N 組才會(huì)觸發(fā)一次刷盤(pán)操作。
也就是說(shuō),執(zhí)行一次刷盤(pán)操作之后,接下來(lái)第 1 ~ N-1 組的 leader 線(xiàn)程都不會(huì)執(zhí)行 binlog 日志刷盤(pán)操作。
等到第 N 組時(shí),它的 leader 線(xiàn)程才會(huì)把第 1 ~ N 組的所有事務(wù)線(xiàn)程產(chǎn)生的 binlog 日志一起刷盤(pán)。
源碼中有一個(gè)變量 sync_counter 用于記錄 sync 子階段自上次刷盤(pán)操作以后,有多少組的 leader 線(xiàn)程沒(méi)有進(jìn)行刷盤(pán)操作。
每當(dāng)有一個(gè) leader 線(xiàn)程沒(méi)有執(zhí)行刷盤(pán)操作,sync_counter 變量的值就會(huì)加 1。
只要有一個(gè) leader 線(xiàn)程執(zhí)行了刷盤(pán)操作,sync_counter 變量的值就會(huì)清零,重新開(kāi)始計(jì)數(shù)。
重點(diǎn)來(lái)了,如果某一組的 leader 線(xiàn)程判斷 sync_counter + 1 >= sync_binlog 條件成立,那么該 leader 線(xiàn)程就要執(zhí)行刷盤(pán)操作,刷盤(pán)之前會(huì)觸發(fā)等待更多事務(wù)線(xiàn)程進(jìn)入 sync 子階段中的等待過(guò)程。
換句大白話(huà)來(lái)說(shuō):sync 子階段 leader 線(xiàn)程把 binlog 日志刷盤(pán)之前會(huì)進(jìn)入等待過(guò)程,目的是為了攢到更多的 follower 線(xiàn)程,能夠把更多的 binlog 日志一起刷盤(pán)。
我們現(xiàn)在知道了 leader 線(xiàn)程為什么要等待,以及什么情況下需要等待,那要等待多長(zhǎng)時(shí)間呢?
等待過(guò)程持續(xù)多長(zhǎng)時(shí)間由 2 個(gè)系統(tǒng)變量控制。
binlog_group_commit_sync_delay,單位為微秒,表示 sync 子階段的 leader 線(xiàn)程在執(zhí)行 binlog 日志文件刷盤(pán)操作之前,需要等待的多少微秒,默認(rèn)值為 0。
如果它的值為 0,表示跳過(guò)等待過(guò)程。
如果它的值大于 0,leader 線(xiàn)程會(huì)等待 binlog_group_commit_sync_delay 毫秒。
但是,在等待過(guò)程中,leader 線(xiàn)程會(huì)每隔一段時(shí)間就去看看 sync 隊(duì)列里的事務(wù)線(xiàn)程數(shù)量是不是大于等于系統(tǒng)變量 binlog_group_commit_sync_no_delay_count 的值。
只要 binlog_group_commit_sync_no_delay_count sync 的值大于 0,并且隊(duì)列里的事務(wù)線(xiàn)程數(shù)量大于等于 該系統(tǒng)變量的值,立馬停止等待,開(kāi)始執(zhí)行第 2 步及之后的操作
檢查 sync 隊(duì)列里事務(wù)線(xiàn)程數(shù)量的時(shí)間間隔是:binlog_group_commit_sync_delay 除以 10 得到的結(jié)果,單位是 微秒,如果得到的結(jié)果是大于 1 微秒,時(shí)間間隔就是 1 微秒。
第 2 步,清空 sync 隊(duì)列。
清空之前,會(huì)先鎖住 sync 隊(duì)列,在這之前進(jìn)入 sync 隊(duì)列的所有事務(wù)線(xiàn)程就成為了一組。
鎖住之后,會(huì)清空 sync 隊(duì)列。
清空之后,進(jìn)入 sync 隊(duì)列的事務(wù)線(xiàn)程就屬于下一組了,在這之后第 1 個(gè)進(jìn)入 sync 隊(duì)列的事務(wù)會(huì)成為下一組的 leader 線(xiàn)程。
第 3 步,binlog 日志文件刷盤(pán)。
刷盤(pán)操作完成后,這一組事務(wù)線(xiàn)程的 binlog 日志都刷新到磁盤(pán),實(shí)現(xiàn)了持久化,它們?cè)僖膊挥脫?dān)心數(shù)據(jù)庫(kù)崩潰了。
在這一步是否會(huì)執(zhí)行刷盤(pán)操作,也是由系統(tǒng)變量 sync_binlog 控制的,在第 1 步中已經(jīng)詳細(xì)介紹過(guò),這里就不重復(fù)了介紹了。
(4)commit 子階段
commit 子階段也有一個(gè)隊(duì)列,是 commit 隊(duì)列。
第 1 個(gè)進(jìn)入 commit 隊(duì)列的事務(wù)線(xiàn)程是 commit 子階段的 leader 線(xiàn)程。
第 2 個(gè)及以后進(jìn)入 commit 隊(duì)列的事務(wù)線(xiàn)程是 commit 子階段的 follower 線(xiàn)程。
sync 子階段完成之后,它的 leader 線(xiàn)程會(huì)進(jìn)入到 commit 子階段,并加入 commit 隊(duì)列,然后再讓它的 follower 線(xiàn)程也逐個(gè)加入 commit 隊(duì)列。
sync 子階段的 leader 線(xiàn)程和 follower 線(xiàn)程都加入到 commit 隊(duì)列之后,leader 線(xiàn)程會(huì)釋放它持有的 Lock_sync 互斥鎖。
如果 sync 子階段的 leader 線(xiàn)程加入 commit 隊(duì)列之前,commit 隊(duì)列是空的,那么它又會(huì)成為 commit 子階段的 leader 線(xiàn)程。
否則,它和它的 follower 線(xiàn)程都會(huì)成為 commit 子階段的 follower 線(xiàn)程。
commit 子階段,leader 線(xiàn)程最重要的工作就是提交事務(wù),然后給所有處于 commit 子階段的 follower 線(xiàn)程發(fā)通知。
leader 線(xiàn)程提交事務(wù),是只提交自己的事務(wù),還是會(huì)把所有 follower 線(xiàn)程的事務(wù)也一起提交了,由系統(tǒng)變量 binlog_order_commits 變量控制。
binlog_order_commits 的默認(rèn)值為 ON,表示 leader 線(xiàn)程除了會(huì)提交自己的事務(wù),還會(huì)提交所有 follower 線(xiàn)程的事務(wù)。
如果 binlog_order_commits 的值為 OFF,表示 leader 線(xiàn)程只會(huì)提交自己的事務(wù)。
leader 線(xiàn)程提交事務(wù)之后,會(huì)通知所有 follower 線(xiàn)程。
follower 線(xiàn)程收到通知之后,會(huì)退出等待狀態(tài),繼續(xù)進(jìn)行接下來(lái)的工作,也就是收尾工作。
每個(gè)事務(wù)線(xiàn)程中都有一個(gè) commit_low 屬性,如果 leader 線(xiàn)程已經(jīng)把 follower 線(xiàn)程的事務(wù)也一起提交了,會(huì)把 follower 線(xiàn)程的該屬性值設(shè)置為 false,follower 線(xiàn)程在執(zhí)行收尾工作的時(shí)候,就不需要再提交自己的事務(wù)了。
如果 leader 線(xiàn)程只提交了自己的事務(wù),而沒(méi)有提交 follower 線(xiàn)程的事務(wù),commit_low 屬性的值為 true,follower 線(xiàn)程在執(zhí)行收尾工作的時(shí)候,需要各自提交自己的事務(wù)。
5、總結(jié)
二階段提交的核心邏輯是把多個(gè)事務(wù)的 Redo 日志合并刷盤(pán),把多個(gè)事務(wù)的 binlog 日志合并刷盤(pán),從而把少量數(shù)據(jù)多次 IO 變?yōu)楦髷?shù)據(jù)更少 IO,最終達(dá)到提升事務(wù)提交效率的目標(biāo)。
最后,以一張二階段提交的整體流程圖作為本文的結(jié)尾:
本文轉(zhuǎn)載自微信公眾號(hào)「一樹(shù)一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹(shù)一溪公眾號(hào)。