1. 程式人生 > >MySQL 日誌系統之 redo log 和 binlog

MySQL 日誌系統之 redo log 和 binlog

之前我們瞭解了一條查詢語句的執行流程,並介紹了執行過程中涉及的處理模組。一條查詢語句的執行過程一般是經過聯結器、分析器、優化器、執行器等功能模組,最後到達儲存引擎。

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

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

CREATE TABLE `T` (
  `ID` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入一條資料:

INSERT INTO T VALUES ('2', '1');

如果要將 ID=2 這一行的 c 的值加 1,SQL 語句為:

UPDATE T SET c = c + 1 WHERE ID = 2;

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

  1. 通過聯結器,客戶端與 MySQL 建立連線
  2. update 語句會把 T 表上的所有查詢快取結果清空
  3. 分析器會通過詞法分析和語法分析識別這是一條更新語句
  4. 優化器會決定使用 ID 這個索引(聚簇索引)
  5. 執行器負責具體執行,找到匹配的一行,然後更新
  6. 更新過程中還會涉及 redo log(重做日誌)和 binlog(歸檔日誌)的操作

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

接下來讓我們分別去研究下日誌模組中的 redo log 和 binlog。

日誌模組:redo log

在 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 的操作。從頭開始寫,寫到末尾就又回到開頭迴圈寫,如下圖所示。

圖中展示了一組 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 區別:

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
  2. redo log 是物理日誌,記錄的是在某個資料頁上做了什麼修改;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯。
  3. redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。追加寫是指 binlog 檔案寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。

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

  1. 執行器先找引擎取 ID=2 這一行。ID 是主鍵,引擎直接用樹搜尋找到這一行。如果 ID=2 這一行所在的資料頁本來就在記憶體中,就直接返回給執行器;否則,需要先從磁碟讀入記憶體,然後再返回。
  2. 執行器拿到引擎給的行資料,把這個值加上 1,比如原來是 N,現在就是 N+1,得到新的一行資料,再呼叫引擎介面寫入這行新資料。
  3. 引擎將這行新資料更新到記憶體(InnoDB Buffer Pool)中,同時將這個更新操作記錄到 redo log 裡面,此時 redo log 處於 prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。
  4. 執行器生成這個操作的 binlog,並把 binlog 寫入磁碟。
  5. 執行器呼叫引擎的提交事務介面,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成。

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

其中將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是兩階段提交(2PC)。

兩階段提交(2PC)

MySQL 使用兩階段提交主要解決 binlog 和 redo log 的資料一致性的問題。

redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。下圖為 MySQL 二階段提交簡圖:

兩階段提交原理描述:

  1. InnoDB redo log 寫盤,InnoDB 事務進入 prepare 狀態。
  2. 如果前面 prepare 成功,binlog 寫盤,那麼再繼續將事務日誌持久化到 binlog,如果持久化成功,那麼 InnoDB 事務則進入 commit 狀態(在 redo log 裡面寫一個 commit 記錄)

備註: 每個事務 binlog 的末尾,會記錄一個 XID event,標誌著事務是否提交成功,也就是說,recovery 過程中,binlog 最後一個 XID event 之後的內容都應該被 purge。

日誌相關問題

怎麼進行資料恢復?

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

  • 首先,找到最近的一次全量備份,從這個備份恢復到臨時庫
  • 然後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到中午誤刪表之前的那個時刻。

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

redo log 和 binlog 是怎麼關聯起來的?

redo log 和 binlog 有一個共同的資料欄位,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而沒有 commit 的 redo log,就拿著 XID 去 binlog 找對應的事務。

MySQL 怎麼知道 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 太小的話,會導致很快就被寫滿,然後不得不強行刷 redo log,這樣 WAL 機制的能力就發揮不出來了。

如果是幾個 TB 的磁碟的話,直接將 redo log 設定為 4 個檔案,每個檔案 1GB。

資料寫入後的最終落盤,是從 redo log 更新過來的還是從 buffer pool 更新過來的呢?

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

  1. 資料頁被修改以後,跟磁碟的資料頁不一致,稱為髒頁。最終資料落盤,就是把記憶體中的資料頁寫盤。這個過程與 redo log 毫無關係。
  2. 在崩潰恢復場景中,InnoDB 如果判斷到一個數據頁可能在崩潰恢復的時候丟失了更新,就會將它讀到記憶體,然後讓 redo log 更新記憶體內容。更新完成後,記憶體頁變成髒頁,就回到了第一種情況的狀態。

redo log buffer 是什麼?是先修改記憶體,還是先寫 redo log 檔案?

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

begin;
INSERT INTO T1 VALUES ('1', '1');
INSERT INTO T2 VALUES ('1', '1');
commit;

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

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

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

以下是我擷取的部分 redo log buffer 的原始碼:

/** redo log buffer */
struct log_t{
    char        pad1[CACHE_LINE_SIZE];
    lsn_t       lsn;        
    ulint       buf_free;   // buffer 內剩餘空間的起始點的 offset
#ifndef UNIV_HOTBACKUP
    char        pad2[CACHE_LINE_SIZE];
    LogSysMutex mutex;      
    LogSysMutex write_mutex;    
    char        pad3[CACHE_LINE_SIZE];
    FlushOrderMutex log_flush_order_mutex;
#endif /* !UNIV_HOTBACKUP */
    byte*       buf_ptr;    // 隱性的 buffer
    byte*       buf;        // 真正操作的 buffer
    bool        first_in_use;   
    ulint       buf_size;   // buffer大小
    bool        check_flush_or_checkpoint;
    UT_LIST_BASE_NODE_T(log_group_t) log_groups;

#ifndef UNIV_HOTBACKUP
    /** The fields involved in the log buffer flush @{ */
    ulint       buf_next_to_write;
    volatile bool   is_extending;   
    lsn_t       write_lsn;  /*!< last written lsn */
    lsn_t       current_flush_lsn;
    lsn_t       flushed_to_disk_lsn;
    ulint       n_pending_flushes;
    os_event_t  flush_event;    
    ulint       n_log_ios;  
    ulint       n_log_ios_old;  
    time_t      last_printout_time;

    /** Fields involved in checkpoints @{ */
    lsn_t       log_group_capacity; 
    lsn_t       max_modified_age_async;
    lsn_t       max_modified_age_sync;
    lsn_t       max_checkpoint_age_async;
    lsn_t       max_checkpoint_age;
    ib_uint64_t next_checkpoint_no;
    lsn_t       last_checkpoint_lsn;
    lsn_t       next_checkpoint_lsn;
    mtr_buf_t*  append_on_checkpoint;
    ulint       n_pending_checkpoint_writes;
    rw_lock_t   checkpoint_lock;
#endif /* !UNIV_HOTBACKUP */
    byte*       checkpoint_buf_ptr;
    byte*       checkpoint_buf; 
    /* @} */
};

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

總結

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

另外還介紹了與 MySQL 日誌系統密切相關的兩階段提交(2PC),兩階段提交是解決分散式系統的一致性問題常用的一個方案,類似的還有 三階段提交(3PC) 和 PAXOS 演算法