天天看點

MySQL 日志系統之 redo log 和 binlog

之前我們了解了一條查詢語句的執行流程,并介紹了執行過程中涉及的處理子產品。一條查詢語句的執行過程一般是經過連接配接器、分析器、優化器、執行器等功能子產品,最後到達存儲引擎。

那麼,一條 sql 更新語句的執行流程又是怎樣的呢?

首先我們建立一個表 user_info,主鍵為 id,建立語句如下:

插入一條資料:

insert into t values ('2', '1');

如果要将 id=2 這一行的 c 的值加 1,sql 語句為:

update t set c = c + 1 where id = 2;

前面介紹過 sql 語句基本的執行鍊路,這裡把那張圖拿過來。因為,更新語句同樣會走一遍查詢語句走的流程。

MySQL 日志系統之 redo log 和 binlog

通過連接配接器,用戶端與 mysql 建立連接配接

update 語句會把 t 表上的所有查詢緩存結果清空

分析器會通過詞法分析和文法分析識别這是一條更新語句

優化器會決定使用 id 這個索引(聚簇索引)

執行器負責具體執行,找到比對的一行,然後更新

更新過程中還會涉及 redo log(重做日志)和 binlog(歸檔日志)的操作

其中,這兩種日志預設在資料庫的 data 目錄下,redo log 是 ib_logfile0 格式的,binlog 是 xxx-bin.000001 格式的。

接下來讓我們分别去研究下日志子產品中的 redo log 和 binlog。

在 mysql 中,如果每一次的更新操作都需要寫進磁盤,然後磁盤也要找到對應的那條記錄,然後再更新,整個過程 io 成本、查找成本都很高。為了解決這個問題,mysql 的設計者就采用了日志(redo log)來提升更新效率。

而日志和磁盤配合的整個過程,其實就是 mysql 裡的 wal 技術,wal 的全稱是 write-ahead logging,它的關鍵點就是先寫日志,再寫磁盤。

具體來說,當有一條記錄需要更新的時候,innodb 引擎就會先把記錄寫到 redo log(redolog buffer)裡面,并更新記憶體(buffer pool),這個時候更新就算完成了。同時,innodb 引擎會在适當的時候(如系統空閑時),将這個操作記錄更新到磁盤裡面(刷髒頁)。

redo log 是 innodb 存儲引擎層的日志,又稱重做日志檔案,redo log 是循環寫的,redo log 不是記錄資料頁更新之後的狀态,而是記錄這個頁做了什麼改動。

redo log 是固定大小的,比如可以配置為一組 4 個檔案,每個檔案的大小是 1gb,那麼日志總共就可以記錄 4gb 的操作。從頭開始寫,寫到末尾就又回到開頭循環寫,如下圖所示。

MySQL 日志系統之 redo log 和 binlog

圖中展示了一組 4 個檔案的 redo log 日志,checkpoint 是目前要擦除的位置,擦除記錄前需要先把對應的資料落盤(更新記憶體頁,等待刷髒頁)。write pos 到 checkpoint 之間的部分可以用來記錄新的操作,如果 write pos 和 checkpoint 相遇,說明 redolog 已滿,這個時候資料庫停止進行資料庫更新語句的執行,轉而進行 redo log 日志同步到磁盤中。checkpoint 到 write pos 之間的部分等待落盤(先更新記憶體頁,然後等待刷髒頁)。

有了 redo log 日志,那麼在資料庫進行異常重新開機的時候,可以根據 redo log 日志進行恢複,也就達到了 crash-safe。

redo log 用于保證 crash-safe 能力。innodb_flush_log_at_trx_commit 這個參數設定成 1 的時候,表示每次事務的 redo log 都直接持久化到磁盤。這個參數建議設定成 1,這樣可以保證 mysql 異常重新開機之後資料不丢失。

日志子產品:binlog

mysql 整體來看,其實就有兩塊:一塊是 server 層,它主要做的是 mysql 功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。redo log 是 innodb 引擎特有的日志,而 server 層也有自己的日志,稱為 binlog(歸檔日志)。

binlog 屬于邏輯日志,是以二進制的形式記錄的是這個語句的原始邏輯,依靠 binlog 是沒有 crash-safe 能力的。

binlog 有兩種模式,statement 格式的話是記 sql 語句,row 格式會記錄行的内容,記兩條,更新前和更新後都有。

sync_binlog 這個參數設定成 1 的時候,表示每次事務的 binlog 都持久化到磁盤。這個參數也建議設定成 1,這樣可以保證 mysql 異常重新開機之後 binlog 不丢失。

為什麼會有兩份日志呢?

因為最開始 mysql 裡并沒有 innodb 引擎。mysql 自帶的引擎是 myisam,但是 myisam 沒有 crash-safe 的能力,binlog 日志隻能用于歸檔。而 innodb 是另一個公司以插件形式引入 mysql 的,既然隻依靠 binlog 是沒有 crash-safe 能力的,是以 innodb 使用另外一套日志系統——也就是 redo log 來實作 crash-safe 能力。

redo log 和 binlog 差別:

redo log 是 innodb 引擎特有的;binlog 是 mysql 的 server 層實作的,所有引擎都可以使用。

redo log 是實體日志,記錄的是在某個資料頁上做了什麼修改;binlog 是邏輯日志,記錄的是這個語句的原始邏輯。

redo log 是循環寫的,空間固定會用完;binlog 是可以追加寫入的。追加寫是指 binlog 檔案寫到一定大小後會切換到下一個,并不會覆寫以前的日志。

有了對這兩個日志的概念性了解後,再來看執行器和 innodb 引擎在執行這個 update 語句時的内部流程。

執行器先找引擎取 id=2 這一行。id 是主鍵,引擎直接用樹搜尋找到這一行。如果 id=2 這一行所在的資料頁本來就在記憶體中,就直接傳回給執行器;否則,需要先從磁盤讀入記憶體,然後再傳回。

執行器拿到引擎給的行資料,把這個值加上 1,比如原來是 n,現在就是 n+1,得到新的一行資料,再調用引擎接口寫入這行新資料。

引擎将這行新資料更新到記憶體(innodb buffer pool)中,同時将這個更新操作記錄到 redo log 裡面,此時 redo log 處于 prepare 狀态。然後告知執行器執行完成了,随時可以送出事務。

執行器生成這個操作的 binlog,并把 binlog 寫入磁盤。

執行器調用引擎的送出事務接口,引擎把剛剛寫入的 redo log 改成送出(commit)狀态,更新完成。

下圖為 update 語句的執行流程圖,圖中灰色框表示是在 innodb 内部執行的,綠色框表示是在執行器中執行的。

MySQL 日志系統之 redo log 和 binlog

其中将 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是兩階段送出(2pc)。

兩階段送出(2pc)

mysql 使用兩階段送出主要解決 binlog 和 redo log 的資料一緻性的問題。

redo log 和 binlog 都可以用于表示事務的送出狀态,而兩階段送出就是讓這兩個狀态保持邏輯上的一緻。下圖為 mysql 二階段送出簡圖:

MySQL 日志系統之 redo log 和 binlog

兩階段送出原理描述:

innodb redo log 寫盤,innodb 事務進入 prepare 狀态。

如果前面 prepare 成功,binlog 寫盤,那麼再繼續将事務日志持久化到 binlog,如果持久化成功,那麼 innodb 事務則進入 commit 狀态(在 redo log 裡面寫一個 commit 記錄)

備注: 每個事務 binlog 的末尾,會記錄一個 xid event,标志着事務是否送出成功,也就是說,recovery 過程中,binlog 最後一個 xid event 之後的内容都應該被 purge。

日志相關問題

binlog 會記錄所有的邏輯操作,并且是采用追加寫的形式。當需要恢複到指定的某一秒時,比如今天下午二點發現中午十二點有一次誤删表,需要找回資料,那你可以這麼做:

首先,找到最近的一次全量備份,從這個備份恢複到臨時庫

然後,從備份的時間點開始,将備份的 binlog 依次取出來,重放到中午誤删表之前的那個時刻。

這樣你的臨時庫就跟誤删之前的線上庫一樣了,然後你可以把表資料從臨時庫取出來,按需要恢複到線上庫去。

redo log 和 binlog 有一個共同的資料字段,叫 xid。崩潰恢複的時候,會按順序掃描 redo log:

如果碰到既有 prepare、又有 commit 的 redo log,就直接送出;

如果碰到隻有 parepare、而沒有 commit 的 redo log,就拿着 xid 去 binlog 找對應的事務。

一個事務的 binlog 是有完整格式的:

statement 格式的 binlog,最後會有 commit

row 格式的 binlog,最後會有一個 xid event

在 mysql 5.6.2 版本以後,還引入了 binlog-checksum 參數,用來驗證 binlog 内容的正确性。對于 binlog 日志由于磁盤原因,可能會在日志中間出錯的情況,mysql 可以通過校驗 checksum 的結果來發現。是以,mysql 是有辦法驗證事務 binlog 的完整性的。

redo log 太小的話,會導緻很快就被寫滿,然後不得不強行刷 redo log,這樣 wal 機制的能力就發揮不出來了。

如果是幾個 tb 的磁盤的話,直接将 redo log 設定為 4 個檔案,每個檔案 1gb。

實際上,redo log 并沒有記錄資料頁的完整資料,是以它并沒有能力自己去更新磁盤資料頁,也就不存在由 redo log 更新過去資料最終落盤的情況。

資料頁被修改以後,跟磁盤的資料頁不一緻,稱為髒頁。最終資料落盤,就是把記憶體中的資料頁寫盤。這個過程與 redo log 毫無關系。

在崩潰恢複場景中,innodb 如果判斷到一個資料頁可能在崩潰恢複的時候丢失了更新,就會将它讀到記憶體,然後讓 redo log 更新記憶體内容。更新完成後,記憶體頁變成髒頁,就回到了第一種情況的狀态。

在一個事務的更新過程中,日志是要寫多次的。比如下面這個事務:

這個事務要往兩個表中插入記錄,插入資料的過程中,生成的日志都得先儲存起來,但又不能在還沒 commit 的時候就直接寫到 redo log 檔案裡。

是以就需要 redo log buffer 出場了,它就是一塊記憶體,用來先存 redo 日志的。也就是說,在執行第一個 insert 的時候,資料的記憶體被修改了,redo log buffer 也寫入了日志。

但是,真正把日志寫到 redo log 檔案,是在執行 commit 語句的時候做的。

以下是我截取的部分 redo log buffer 的源代碼:

redo log buffer 本質上隻是一個 byte 數組,但是為了維護這個 buffer 還需要設定很多其他的 meta data,這些 meta data 全部封裝在 log_t 結構體中。

總結

這篇文章主要介紹了 mysql 裡面最重要的兩個日志,即實體日志 redo log(重做日志)和邏輯日志 binlog(歸檔日志),還講解了有與日志相關的一些問題。

另外還介紹了與 mysql 日志系統密切相關的兩階段送出(2pc),兩階段送出是解決分布式系統的一緻性問題常用的一個方案,類似的還有 三階段送出(3pc) 和 paxos 算法。

繼續閱讀