MySQL redo log 的深度解析
redo log也就是所謂的重做日志,是innoDb存儲(chǔ)引擎獨(dú)有的日志,它使得MySQL在宕機(jī)情況下依舊可以redo log完成數(shù)據(jù)具備恢復(fù)能力, 從而保證數(shù)據(jù)完整性,本文將針對(duì)該日志進(jìn)行分析講解,希望對(duì)你有幫助。
1. redo log的作用
redo log是InnoDB存儲(chǔ)引擎獨(dú)有的日志,用于MySQL工作過程中崩潰或者宕機(jī)時(shí)進(jìn)行數(shù)據(jù)恢復(fù)的文件,從而保證數(shù)據(jù)的持久性以及完整性。
2. redo log是如何運(yùn)行工作的
我們都知道數(shù)據(jù)庫(kù)數(shù)據(jù)基本單位也是和操作系統(tǒng)一致的,都是以頁(yè)為單位,我們以MySQL數(shù)據(jù)查詢?yōu)槔?,為了盡可能減少IO次數(shù),MySQL在進(jìn)行數(shù)據(jù)查詢會(huì)優(yōu)先將數(shù)據(jù)查詢并存儲(chǔ)到Buffer Pool中,然后按照一定的調(diào)度規(guī)則將修改操作寫回磁盤中。
當(dāng)我們需要對(duì)數(shù)據(jù)修改操作之后,這個(gè)修改操作就會(huì)優(yōu)先被生成一個(gè)redo日志存放到redo log buffer中,最終就會(huì)被刷盤并寫入到redo log file中。
默認(rèn)情況下日志對(duì)應(yīng)的緩沖區(qū)大小為16M,這里面包括了redo.log的緩沖區(qū),該變量我們可以通過如下語(yǔ)句查看:
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
對(duì)應(yīng)查詢結(jié)果如下:
Variable_name |Value |
----------------------+--------+
innodb_log_buffer_size|16777216|
redo log的刷盤時(shí)機(jī)
上文圖解的第四步提到了redo log刷盤的操作,當(dāng)符合以下幾種條件時(shí),對(duì)應(yīng)redo log buffer會(huì)被刷盤持久化到磁盤中:
- 事務(wù)提交:當(dāng)事務(wù)提交時(shí),log buffer里redo log會(huì)按照innodb_flush_log_at_trx_commit的刷盤時(shí)機(jī)將數(shù)據(jù)持久化到磁盤中。
- log buffer空間不足:log buffer中的redo log已經(jīng)占滿該緩沖區(qū)一半時(shí),緩沖區(qū)數(shù)據(jù)就會(huì)被刷到磁盤中。
- 事務(wù)日志緩沖區(qū)已滿:InnoDB使用一個(gè)事務(wù)日志緩沖區(qū)(transaction log buffer)存儲(chǔ)事務(wù)redo log的日志條目,當(dāng)該緩存區(qū)已滿時(shí),就會(huì)觸發(fā)日志刷新將日志寫入磁盤中。
- checkpoint:線程會(huì)定時(shí)執(zhí)行一個(gè)checkpoint,將buffer pool已經(jīng)刷盤對(duì)應(yīng)的redo.log設(shè)置為可被覆蓋(保證日志空間可以循環(huán)復(fù)用),這期間對(duì)應(yīng)的redo數(shù)據(jù)就會(huì)被寫入磁盤中。
- 服務(wù)器關(guān)閉:MySQL服務(wù)正常關(guān)閉時(shí),這些緩沖區(qū)的數(shù)據(jù)就會(huì)寫入到磁盤中。
redo log的刷盤策略
上文事務(wù)提交時(shí)提到一個(gè)刷盤策略的概念,實(shí)際上寫入磁盤的時(shí)機(jī)是由MySQL系統(tǒng)參數(shù)設(shè)置決定的,我們可以鍵入下面這條SQL查看innodb_flush_log_at_trx_commit這個(gè)參數(shù)的設(shè)定值:
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
以筆者的MySQL8為例,默認(rèn)情況下這個(gè)參數(shù)值為1:
當(dāng)這個(gè)值為0時(shí),每次進(jìn)行修改寫入到redo log buffer,然后redo log buffer會(huì)將數(shù)據(jù)寫到page cache中,由log thread每個(gè)1s調(diào)用操作系統(tǒng)函數(shù)fsync將數(shù)據(jù)寫入到redo.file中。很可能因?yàn)榉?wù)器崩潰或者宕機(jī)導(dǎo)致丟失1s的數(shù)據(jù)。
1為默認(rèn)值,當(dāng)參數(shù)值設(shè)置為1時(shí), 每次進(jìn)行修改操作后將數(shù)據(jù)寫入到redo log buffer中,一旦事務(wù)被提交,就會(huì)自動(dòng)調(diào)用操作系統(tǒng)函數(shù)fsync將數(shù)據(jù)寫入的磁盤中的redo.file文件中。若設(shè)置為這個(gè)級(jí)別,當(dāng)服務(wù)器宕機(jī),若當(dāng)前事務(wù)沒有提交,這部分?jǐn)?shù)據(jù)丟失也無妨,事務(wù)提交的話,那么這個(gè)操作就會(huì)被寫到磁盤中,照樣可以恢復(fù)。
配置為2時(shí),每當(dāng)事務(wù)提交后,redo log就會(huì)刷入內(nèi)核緩沖區(qū),這些數(shù)據(jù)具體何時(shí)刷盤則交由操作系統(tǒng)決定,這種情況在MySQL宕機(jī)情況下不會(huì)造成數(shù)據(jù)丟失,一旦操作系統(tǒng)崩潰則可能會(huì)造成內(nèi)核緩沖區(qū)的redo log數(shù)據(jù)丟失,導(dǎo)致進(jìn)行數(shù)據(jù)備份還原時(shí)丟失一部分?jǐn)?shù)據(jù):
redo log的日志文件組
redo log并不是單指一個(gè)文件,它是由一組日志文件構(gòu)成的,如下圖所示,這些文件大小都是一樣的,寫入操作時(shí)依次從從1開始寫,文件1寫滿了,就將數(shù)據(jù)寫到文件2,最后寫到文件4。
redolog通過write pos標(biāo)記當(dāng)前寫入的位置,每次完成寫入write pos標(biāo)志位后移,一旦write pos和checkpoint相遇時(shí)就說明文件滿了,此時(shí)innodb就會(huì)通過讓checkpoint往后移進(jìn)行一些空間數(shù)據(jù)擦除,以此來保證一個(gè)足夠空間容納新數(shù)據(jù)。
為什么InnoDB不直接將數(shù)據(jù)寫入磁盤
頁(yè)是操作系統(tǒng)的基本單位,一頁(yè)差不多16kb,而我們每次操作的數(shù)據(jù)可能也就x byte,為了x byte的數(shù)據(jù)操作將一頁(yè)的數(shù)據(jù)進(jìn)行同步持久化實(shí)在有些大材小用了,所以通過redo log buffer記錄修改內(nèi)容,通過刷盤策略進(jìn)行數(shù)據(jù)輸盤更新,由此提升數(shù)據(jù)庫(kù)的并發(fā)能力,
bin.log和redo.log對(duì)應(yīng)的二階段提交
經(jīng)常有讀者面試被問道的為什么我有了undo log,你還需要bin log呢?而且這兩個(gè)日志我到底要先寫哪個(gè)才能保證主從數(shù)據(jù)庫(kù)的一致性呢?
對(duì)此我們不妨用反正法來說明:
假設(shè)我們先寫bin.log,當(dāng)事務(wù)提交后bin.log寫入成功,結(jié)果再寫redo.log期間,數(shù)據(jù)庫(kù)掛了。重啟恢復(fù)后,主數(shù)據(jù)庫(kù)工具redo.log恢復(fù)到bin log寫入前的樣子,而從數(shù)據(jù)庫(kù)在工具bin.log進(jìn)行數(shù)據(jù)同步時(shí)發(fā)現(xiàn)bin log有一條寫入操作,最終從數(shù)據(jù)庫(kù)比主數(shù)據(jù)庫(kù)多了一條數(shù)據(jù)。
我們?cè)偌僭O(shè)寫redo log,假設(shè)事務(wù)執(zhí)行期間我們就寫了redo log,在事務(wù)提交之后寫bin log數(shù)據(jù)庫(kù)掛了,我們重啟數(shù)據(jù)庫(kù)后主主庫(kù)恢復(fù)。主庫(kù)根據(jù)redo log進(jìn)行災(zāi)備恢復(fù),將我們更新的數(shù)據(jù)同時(shí)恢復(fù)回來,而從庫(kù)根據(jù)bin log進(jìn)行數(shù)據(jù)同步時(shí),并沒有察覺到主庫(kù)剛剛寫入的數(shù)據(jù),這就導(dǎo)致了從庫(kù)比主庫(kù)少了一條數(shù)據(jù)。
所以MySQL設(shè)計(jì)者提出了二階段提交的概念,整體步驟為:
- 在事務(wù)開始時(shí),先寫redo-log(prepare)。
- 事務(wù)提交時(shí),再寫bin log。
- 事務(wù)提交成功,再寫redo-log(commit)。
有了這樣一個(gè)整體步驟我們不妨用兩種情況來舉個(gè)例子演示一下二階段提交如何保證數(shù)據(jù)一致性。
假設(shè)我們有一張user表,這張表只有id、name兩個(gè)字段。我們執(zhí)行如下SQL:
update user set name='aa' where id=1;
假如我們?cè)趓edo.log提交時(shí)數(shù)據(jù)庫(kù)宕機(jī),二階段是如何保證數(shù)據(jù)一致性的呢?
首先數(shù)據(jù)庫(kù)重啟恢復(fù),然后主庫(kù)發(fā)現(xiàn)redo.log日志處于prepare而且bin.log也沒有寫入,所以一切恢復(fù)到之前的樣子(事務(wù)回滾),而從庫(kù)對(duì)此無感,同步時(shí)也是同步成操作失敗之前的樣子,一切風(fēng)平浪靜:
假如我們bin.log進(jìn)行commit成功之后數(shù)據(jù)庫(kù)宕機(jī),二階段提交是如何保證數(shù)據(jù)庫(kù)一致性的呢?還是老規(guī)矩:
- 數(shù)據(jù)庫(kù)重啟恢復(fù),然后主庫(kù)發(fā)現(xiàn)bin.log有個(gè)commit成功的數(shù)據(jù)(事務(wù)是完整的)
- 然redo.log處于prepare階段,但是我們還是可以根據(jù)情況推斷出有個(gè)當(dāng)前主庫(kù)有個(gè)commit成功的事務(wù),所以redo.log會(huì)根據(jù)bin.log將redo.log設(shè)置為commit
- 從庫(kù)已根據(jù)主庫(kù)的bin.log發(fā)現(xiàn)有新增一條新數(shù)據(jù),由此同步一條更新數(shù)據(jù),雙方都有了一條新數(shù)據(jù),數(shù)據(jù)庫(kù)一致性由此保證: