美團(tuán)二面:如何解決 Bin Log 與 Redo Log 的一致性問(wèn)題
剛看見(jiàn)這個(gè)題目的時(shí)候還是有點(diǎn)懵逼的,后來(lái)才反應(yīng)過(guò)來(lái)其實(shí)問(wèn)的就是 redo log 的兩階段提交
老規(guī)矩,背誦版在文末。點(diǎn)擊閱讀原文可以直達(dá)我收錄整理的各大廠面試真題
為什么說(shuō) redo log 具有崩潰恢復(fù)的能力
前面我們說(shuō)過(guò),MySQL Server 層擁有的 bin log 只能用于歸檔,不足以實(shí)現(xiàn)崩潰恢復(fù)(crash-safe),需要借助 InnoDB 引擎的 redo log 才能擁有崩潰恢復(fù)的能力。所謂崩潰恢復(fù)就是:即使在數(shù)據(jù)庫(kù)宕機(jī)的情況下,也不會(huì)出現(xiàn)操作一半的情況
至于為什么說(shuō) redo log 具有崩潰恢復(fù)的能力,而 bin log 沒(méi)有,我們先來(lái)簡(jiǎn)單看一下這兩種日志有哪些不同點(diǎn):
1)適用對(duì)象不同:
bin log 是 MySQL 的 Server 層實(shí)現(xiàn)的,所有引擎都可以使用
而 redo log 是 InnoDB 引擎特有的
2)寫入內(nèi)容不同:
bin log 是邏輯日志,記錄的是這個(gè)語(yǔ)句的原始邏輯,比如 “給 id = 1 這一行的 age 字段加 1”
redo log 是物理日志,記錄的是 “在某個(gè)數(shù)據(jù)頁(yè)上做了什么修改”
3)寫入方式不同:
bin log 是可以追加寫入的。“追加寫” 是指 bin log 文件寫到一定大小后會(huì)切換到下一個(gè),并不會(huì)覆蓋以前的日志
redo log 是循環(huán)寫的,空間固定會(huì)被用完
可以看到,redo log 和 bin log 的一個(gè)很大的區(qū)別就是,一個(gè)是循環(huán)寫,一個(gè)是追加寫。也就是說(shuō) redo log 只會(huì)記錄未刷入磁盤的日志,已經(jīng)刷入磁盤的數(shù)據(jù)都會(huì)從 redo log 這個(gè)有限大小的日志文件里刪除。
而 bin log 是追加日志,保存的是全量的日志。這就會(huì)導(dǎo)致一個(gè)問(wèn)題,那就是沒(méi)有標(biāo)志能讓 InnoDB 從 bin log 中判斷哪些數(shù)據(jù)已經(jīng)刷入磁盤了,哪些數(shù)據(jù)還沒(méi)有。
舉個(gè)例子,bin log 記錄了兩條日志:
- 記錄 1:給 id = 1 這一行的 age 字段加 1
- 記錄 2:給 id = 1 這一行的 age 字段加 1
假設(shè)在記錄 1 刷盤后,記錄 2 未刷盤時(shí),數(shù)據(jù)庫(kù)崩潰。重啟后,只通過(guò) bin log 數(shù)據(jù)庫(kù)是無(wú)法判斷這兩條記錄哪條已經(jīng)寫入磁盤,哪條沒(méi)有寫入磁盤,不管是兩條都恢復(fù)至內(nèi)存,還是都不恢復(fù),對(duì) id = 1 這行數(shù)據(jù)來(lái)說(shuō),都是不對(duì)的。
但 redo log 不一樣,只要刷入磁盤的數(shù)據(jù),都會(huì)從 redo log 中被抹掉,數(shù)據(jù)庫(kù)重啟后,直接把 redo log 中的數(shù)據(jù)都恢復(fù)至內(nèi)存就可以了。
這就是為什么說(shuō) redo log 具有崩潰恢復(fù)的能力,而 bin log 不具備。
redo log 兩階段提交
前面我們介紹過(guò)一條 SQL 查詢語(yǔ)句的執(zhí)行過(guò)程,簡(jiǎn)單回顧:
MySQL 客戶端與服務(wù)器間建立連接,客戶端發(fā)送一條查詢給服務(wù)器;
服務(wù)器先檢查查詢緩存,如果命中了緩存,則立刻返回存儲(chǔ)在緩存中的結(jié)果;否則進(jìn)入下一階段;
服務(wù)器端進(jìn)行 SQL 解析、預(yù)處理,生成合法的解析樹(shù);
再由優(yōu)化器生成對(duì)應(yīng)的執(zhí)行計(jì)劃;
執(zhí)行器根據(jù)優(yōu)化器生成的執(zhí)行計(jì)劃,調(diào)用相應(yīng)的存儲(chǔ)引擎的 API 來(lái)執(zhí)行,并將執(zhí)行結(jié)果返回給客戶端
對(duì)于更新語(yǔ)句來(lái)說(shuō),這套流程同樣也是要走一遍的,不同的是,更新流程還涉及兩個(gè)重要的日志模塊 bin log 和 redo log。
以下面這條簡(jiǎn)單的 SQL 語(yǔ)句為例,我們來(lái)解釋下執(zhí)行器和 InnoDB 存儲(chǔ)引擎在更新時(shí)做了哪些事情:
- update table set age = age + 1 where id = 1;
執(zhí)行器:找存儲(chǔ)引擎取到 id = 1 這一行記錄
存儲(chǔ)引擎:根據(jù)主鍵索引樹(shù)找到這一行,如果 id = 1 這一行所在的數(shù)據(jù)頁(yè)本來(lái)就在內(nèi)存池(Buffer Pool)中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存池,然后再返回
執(zhí)行器:拿到存儲(chǔ)引擎返回的行記錄,把 age 字段加上 1,得到一行新的記錄,然后再調(diào)用存儲(chǔ)引擎的接口寫入這行新記錄
存儲(chǔ)引擎:將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到 redo log 里面,此時(shí) redo log 處于 prepare 狀態(tài)。然后告知執(zhí)行器執(zhí)行完成了,隨時(shí)可以提交事務(wù)
注意不要把這里的提交事務(wù)和我們 sql 語(yǔ)句中的提交事務(wù) commit 命令搞混了哈,我們這里說(shuō)的提交事務(wù),指的是事務(wù)提交過(guò)程中的一個(gè)小步驟,也是最后一步。當(dāng)這個(gè)步驟執(zhí)行完成后,commit 命令就執(zhí)行成功了。
執(zhí)行器:生成這個(gè)操作的 bin log,并把 bin log 寫入磁盤
執(zhí)行器:調(diào)用存儲(chǔ)引擎的提交事務(wù)接口
存儲(chǔ)引擎:把剛剛寫入的 redo log 狀態(tài)改成提交(commit)狀態(tài),更新完成
如下圖所示:
可以看到,所謂兩階段提交,其實(shí)就是把 redo log 的寫入拆分成了兩個(gè)步驟:prepare 和 commit。
所以,為什么要這樣設(shè)計(jì)呢?這樣設(shè)計(jì)怎么就能夠?qū)崿F(xiàn)崩潰恢復(fù)呢?
根據(jù)兩階段提交,崩潰恢復(fù)時(shí)的判斷規(guī)則是這樣的:
如果 redo log 里面的事務(wù)是完整的,也就是已經(jīng)有了 commit 標(biāo)識(shí),則直接提交
如果 redo log 里面的事務(wù)處于 prepare 狀態(tài),則判斷對(duì)應(yīng)的事務(wù) binlog 是否存在并完整
- a. 如果 binlog 存在并完整,則提交事務(wù);
- b. 否則,回滾事務(wù)。
當(dāng)然,這樣說(shuō)小伙伴們肯定沒(méi)法理解,下面來(lái)看幾個(gè)實(shí)際的例子:
如下圖所示,假設(shè)數(shù)據(jù)庫(kù)在寫入 redo log(prepare) 階段之后、寫入 binlog 之前,發(fā)生了崩潰,此時(shí) redo log 里面的事務(wù)處于 prepare 狀態(tài),binlog 還沒(méi)寫(對(duì)應(yīng) 2b),所以崩潰的時(shí)候,這個(gè)事務(wù)會(huì)回滾。
Why?
因?yàn)?binlog 還沒(méi)有寫入,之后從庫(kù)進(jìn)行同步的時(shí)候,無(wú)法執(zhí)行這個(gè)操作,但是實(shí)際上主庫(kù)已經(jīng)完成了這個(gè)操作,所以為了主備一致,在主庫(kù)上需要回滾這個(gè)事務(wù)
并且,由于 binlog 還沒(méi)寫,所以也就不會(huì)傳到備庫(kù),從而避免主備不一致的情況。
而如果數(shù)據(jù)庫(kù)在寫入 binlog 之后,redo log 狀態(tài)修改為 commit 前發(fā)生崩潰,此時(shí) redo log 里面的事務(wù)仍然是 prepare 狀態(tài),binlog 存在并完整(對(duì)應(yīng) 2a),所以即使在這個(gè)時(shí)刻數(shù)據(jù)庫(kù)崩潰了,事務(wù)仍然會(huì)被正常提交。
Why?
因?yàn)?binlog 已經(jīng)寫入成功了,這樣之后就會(huì)被從庫(kù)同步過(guò)去,但是實(shí)際上主庫(kù)并沒(méi)有完成這個(gè)操作,所以為了主備一致,在主庫(kù)上需要提交這個(gè)事務(wù)。
所以,其實(shí)可以看出來(lái),處于 prepare 階段的 redo log 加上完整的 bin log,就能保證數(shù)據(jù)庫(kù)的崩潰恢復(fù)了。
可能有同學(xué)就會(huì)問(wèn)了,MySQL 咋知道 bin log 是不是完整的?
簡(jiǎn)單來(lái)說(shuō),一個(gè)事務(wù)的 binlog 是有完整格式的(這個(gè)我們?cè)诤竺娴奈恼轮袝?huì)詳細(xì)解釋):
- statement 格式的 bin log,最后會(huì)有 COMMIT
- row 格式的 bin log,最后會(huì)有 XID event
而對(duì)于 bin log 可能會(huì)在中間出錯(cuò)的情況,MySQL 5.6.2 版本以后引入了 binlog-checksum 參數(shù),用來(lái)驗(yàn)證 bin log 內(nèi)容的正確性。
思考一個(gè)問(wèn)題,兩階段提交是必要的嗎?可不可以先 redo log 寫完,再寫 bin log 或者反過(guò)來(lái)?
1)對(duì)于先寫完 redo log 后寫 bin log 的情況:
假設(shè)在 redo log 寫完,bin log 還沒(méi)有寫完的時(shí)候,MySQL 崩潰。主庫(kù)中的數(shù)據(jù)確實(shí)已經(jīng)被修改了,但是這時(shí)候 bin log 里面并沒(méi)有記錄這個(gè)語(yǔ)句。因此,從庫(kù)同步的時(shí)候,就會(huì)丟失這個(gè)更新,和主庫(kù)不一致。
2)對(duì)于先寫完 binlog 后寫 redo log 的情況:
如果在 bin log 寫完,redo log 還沒(méi)寫的時(shí)候,MySQL 崩潰。因?yàn)?binlog 已經(jīng)寫入成功了,這樣之后就會(huì)被從庫(kù)同步過(guò)去,但是實(shí)際上 redo log 還沒(méi)寫,主庫(kù)并沒(méi)有完成這個(gè)操作,所以從庫(kù)相比主庫(kù)就會(huì)多執(zhí)行一個(gè)事務(wù),導(dǎo)致主備不一致
最后放上這道題的背誦版:
面試官:
- 問(wèn)法 1:如何解決 bin log 與 redo log 的一致性問(wèn)題?
- 問(wèn)法 2:一條 SQL 更新語(yǔ)句是如何執(zhí)行的?
- 問(wèn)法 3:講一下 redo log / redo log 兩階段提交原理
小牛肉:
所謂兩階段提交,其實(shí)就是把 redo log 的寫入拆分成了兩個(gè)步驟:prepare 和 commit。
首先,存儲(chǔ)引擎將執(zhí)行更新好的新數(shù)據(jù)存到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到 redo log 里面,此時(shí) redo log 處于 prepare 狀態(tài)。然后告知執(zhí)行器執(zhí)行完成了,隨時(shí)可以提交事務(wù)
然后執(zhí)行器生成這個(gè)操作的 bin log,并把 bin log 寫入磁盤
最后執(zhí)行器調(diào)用存儲(chǔ)引擎的提交事務(wù)接口,存儲(chǔ)引擎把剛剛寫入的 redo log 狀態(tài)改成提交(commit)狀態(tài),更新完成
如果數(shù)據(jù)庫(kù)在寫入 redo log(prepare) 階段之后、寫入 binlog 之前,發(fā)生了崩潰:
此時(shí) redo log 里面的事務(wù)處于 prepare 狀態(tài),binlog 還沒(méi)寫,之后從庫(kù)進(jìn)行同步的時(shí)候,無(wú)法執(zhí)行這個(gè)操作,但是實(shí)際上主庫(kù)已經(jīng)完成了這個(gè)操作,所以為了主備一致,MySQL 崩潰時(shí)會(huì)在主庫(kù)上回滾這個(gè)事務(wù)
而如果數(shù)據(jù)庫(kù)在寫入 binlog 之后,redo log 狀態(tài)修改為 commit 前發(fā)生崩潰,此時(shí) redo log 里面的事務(wù)仍然是 prepare 狀態(tài),binlog 存在并完整,這樣之后就會(huì)被從庫(kù)同步過(guò)去,但是實(shí)際上主庫(kù)并沒(méi)有完成這個(gè)操作,所以為了主備一致,即使在這個(gè)時(shí)刻數(shù)據(jù)庫(kù)崩潰了,主庫(kù)上事務(wù)仍然會(huì)被正常提交。