MySQL 中 6 個常見的日誌問題
文章出自極客時間 《 MySQL 實戰 45 講》 專欄
MySQL 裡有兩個日誌,即:重做日誌(redo log)和歸檔日誌(binlog)。
其中,binlog 可以給備庫使用,也可以儲存起來用於恢復資料庫歷史資料。它是實現在 server 層的,所有引擎可以共用。redo log 是 InnoDB 特有的日誌,用來支援 crash-safe 能力。
你一定聽過 MySQL 事務的兩階段提交,指的就是在事務提交的時候,分成 prepare 和 commit 兩個階段。
如圖所示為一個事務的執行流程,你在最後三步可以看到,redo log 先 prepare 完成,再寫 binlog,最後才進入 redo log commit 階段。

這裡,我要先和你解釋一個誤會式的問題:這個圖不就是一個 update 語句的執行流程嗎,怎麼還會呼叫 commit 語句?
通常情況下,你會產生這個疑問的原因,在於把兩個“commit”的概念混淆了:
- 問題中的“commit 語句”,是指 MySQL 語法中,用於提交一個事務的命令。一般跟 begin/start transaction 配對使用。
- 而我們圖中用到的這個“commit 步驟”,指的是事務提交過程中的一個小步驟,也是最後一步。當這個步驟執行完成後,這個事務就提交完成了。
- “commit 語句”執行的時候,會包含“commit 步驟”。
而我們這個例子裡面,沒有顯式地開啟事務,因此這個 update 語句自己就是一個事務,在執行完成後提交事務時,就會用到這個“commit 步驟”。
接下來,我們就一起分析一下在兩階段提交的不同時刻,MySQL 異常重啟會出現什麼現象。
如果在圖中時刻 A 的地方,也就是寫入 redo log 處於 prepare 階段之後、寫 binlog 之前,發生了崩潰(crash),由於此時 binlog 還沒寫,redo log 也還沒提交,所以崩潰恢復的時候,這個事務會回滾。這時候,binlog 還沒寫,所以也不會傳到備庫。到這裡,我們都可以理解。
而我們理解會出現問題的地方,主要集中在時刻 B,也就是 binlog 寫完,redo log 還沒 commit 前發生 crash,那崩潰恢復的時候 MySQL 會怎麼處理?
我們先來看一下崩潰恢復時的判斷規則。
1. 如果 redo log 裡面的事務是完整的,也就是已經有了 commit 標識,則直接提交;
2. 如果 redo log 裡面的事務只有完整的 prepare,則判斷對應的事務 binlog 是否存在並完整:
a. 如果是,則提交事務;
b. 否則,回滾事務。
這裡,時刻 B 發生 crash 對應的就是 2(a) 的情況,崩潰恢復過程中事務會被提交。
現在,我們就針對兩階段提交再繼續延展一下。
問題 1:MySQL 怎麼知道 binlog 是完整的?
回答:一個事務的 binlog 是有完整格式的:
- statement 格式的 binlog,最後會有 COMMIT;
- row 格式的 binlog,最後會有一個 XID event。
另外,在 MySQL 5.6.2 版本以後,還引入了 binlog-checksum 引數,用來驗證 binlog 內容的正確性。對於 binlog 日誌由於磁碟原因,可能會在日誌中間出錯的情況,MySQL 可以通過校驗 checksum 的結果來發現。所以,MySQL 還是有辦法驗證事務 binlog 的完整性的。
問題 2:redo log 和 binlog 是怎麼關聯起來的?
回答:它們有一個共同的資料欄位,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:
- 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
- 如果碰到只有 parepare、而沒有 commit 的 redo log,就拿著 XID 去 binlog 找對應的事務。
問題 3:處於 prepare 階段的 redo log 加上完整 binlog,重啟就能恢復,MySQL 為什麼要這麼設計?
回答:其實,這個問題還是跟我們在反證法中說到的資料與備份的一致性有關。在時刻 B,也就是 binlog 寫完以後 MySQL 發生崩潰,這時候 binlog 已經寫入了,之後就會被從庫(或者用這個 binlog 恢復出來的庫)使用。
所以,在主庫上也要提交這個事務。採用這個策略,主庫和備庫的資料就保證了一致性。
問題 4:如果這樣的話,為什麼還要兩階段提交呢?乾脆先 redo log 寫完,再寫 binlog。崩潰恢復的時候,必須得兩個日誌都完整才可以。是不是一樣的邏輯?
回答:其實,兩階段提交是經典的分散式系統問題,並不是 MySQL 獨有的。
如果必須要舉一個場景,來說明這麼做的必要性的話,那就是事務的永續性問題。
對於 InnoDB 引擎來說,如果 redo log 提交完成了,事務就不能回滾(如果這還允許回滾,就可能覆蓋掉別的事務的更新)。而如果 redo log 直接提交,然後 binlog 寫入的時候失敗,InnoDB 又回滾不了,資料和 binlog 日誌又不一致了。
兩階段提交就是為了給所有人一個機會,當每個人都說“我 ok”的時候,再一起提交。
問題 5:不引入兩個日誌,也就沒有兩階段提交的必要了。只用 binlog 來支援崩潰恢復,又能支援歸檔,不就可以了?
回答:我把這個問題再翻譯一下的話,是說只保留 binlog,然後可以把提交流程改成這樣:… -> “資料更新到記憶體” -> “寫 binlog” -> “提交事務”,是不是也可以提供崩潰恢復的能力?
答案是不可以。
如果說歷史原因的話,那就是 InnoDB 並不是 MySQL 的原生儲存引擎。MySQL 的原生引擎是 MyISAM,設計之初就有沒有支援崩潰恢復。
InnoDB 在作為 MySQL 的外掛加入 MySQL 引擎家族之前,就已經是一個提供了崩潰恢復和事務支援的引擎了。
InnoDB 接入了 MySQL 後,發現既然 binlog 沒有崩潰恢復的能力,那就用 InnoDB 原有的 redo log 好了。
而如果說實現上的原因的話,就有很多了。就按照問題中說的,只用 binlog 來實現崩潰恢復的流程,我畫了一張示意圖,這裡就沒有 redo log 了。

這樣的流程下,binlog 還是不能支援崩潰恢復的。我說一個不支援的點吧:binlog 沒有能力恢復“資料頁”。
如果在圖中標的位置,也就是 binlog2 寫完了,但是整個事務還沒有 commit 的時候,MySQL 發生了 crash。
重啟後,引擎內部事務 2 會回滾,然後應用 binlog2 可以補回來;但是對於事務 1 來說,系統已經認為提交完成了,不會再應用一次 binlog1。
但是,InnoDB 引擎使用的是 WAL 技術,執行事務的時候,寫完記憶體和日誌,事務就算完成了。如果之後崩潰,要依賴於日誌來恢復資料頁。
也就是說在圖中這個位置發生崩潰的話,事務 1 也是可能丟失了的,而且是資料頁級的丟失。此時,binlog 裡面並沒有記錄資料頁的更新細節,是補不回來的。
你如果要說,那我優化一下 binlog 的內容,讓它來記錄資料頁的更改可以嗎?可以,但這其實就是又做了一個 redo log 出來。
所以,至少現在的 binlog 能力,還不能支援崩潰恢復。
問題 6:那能不能反過來,只用 redo log,不要 binlog?
回答:如果只從崩潰恢復的角度來講是可以的。你可以把 binlog 關掉,這樣就沒有兩階段提交了,但系統依然是 crash-safe 的。
但是,如果你瞭解一下業界各個公司的使用場景的話,就會發現在正式的生產庫上,binlog 都是開著的。因為 binlog 有著 redo log 無法替代的功能。
一個是歸檔。redo log 是迴圈寫,寫到末尾是要回到開頭繼續寫的。這樣歷史日誌沒法保留,redo log 也就起不到歸檔的作用。
一個就是 MySQL 系統依賴於 binlog。binlog 作為 MySQL 一開始就有的功能,被用在了很多地方。其中,MySQL 系統高可用的基礎,就是 binlog 複製。
還有很多公司有異構系統(比如一些資料分析系統),這些系統就靠消費 MySQL 的 binlog 來更新自己的資料。關掉 binlog 的話,這些下游系統就沒法輸入了。
總之,由於現在包括 MySQL 高可用在內的很多系統機制都依賴於 binlog,所以“鳩佔鵲巢” redo log 還做不到。你看,發展生態是多麼重要。
最後,推薦你關注丁奇的 《MySQL 實戰 45 講》 專欄。在專欄裡,丁奇會幫你梳理出學習 MySQL 的主線知識,比如事務、索引、鎖等,還會就開發過程中經常遇到的具體問題和你分析討論,並且幫你理解問題背後的本質。你會收穫 MySQL 核心技術詳解與原理說明和 36 個 MySQL 常見痛點問題解析。