MySQL的各種日誌

Photo by hippopx.comPhoto by hippopx.com

《MySQL實戰45講》筆記。

1. redo log——只是一塊粉板

孔乙己又來酒館喝酒,兜裏沒錢手機也沒電了,只能向掌櫃的賒賬。掌櫃有一塊粉板,當客人要賒賬的時候就往上寫一筆,等客人少的時候或者粉板寫滿了就記到賬本里去。還好有這塊粉板,不然每次客人要賒賬,掌櫃都要翻看賬本,在密密麻麻的賬本里找到賒賬客人的名字絕對不是一件容易的事,有了粉板,掌櫃只要往粉板上記一筆:“孔乙己 賒 兩文”,空閒的時候再更新到賬本里去,簡單多了。

同樣的,MySQL也有一塊“粉板”—— redo log。更新的時候,先寫到 redo log 和內存裏,這次更新就算是結束了。等到合適的時機再寫到磁盤裏,大大減小了寫磁盤的次數。

redo log 是固定大小、“循環寫”的,就像粉板一樣,頂多也就記個十幾二十條,多了就記不下了,這時會把粉板上的帳都寫到賬本里,再擦掉粉板,從頭開始記。假設 redo log 配置了4組文件,每個文件 1G ,一共可記錄 4G 的操作,寫滿了就會擦掉一部分記錄。

redo log 是物理日誌,記錄的是“在某個數據頁上做了什麼修改”。

有了 redo log,InnoDB 就可以保證即使數據庫發生了異常重啓,之前提交的記錄都不會丟失,這個能力稱爲 crash-safe

2. binlog

binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。

binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如”給 ID=2 這一行的 c 字段加1“。

binlog 是“追加寫”的,一個文件寫完了會切換到下一個,不會覆蓋以前的日誌。

爲什麼有了 redo log 還需要 binlog?

其實 redo log 纔是那個新來的仔。MySQL 自帶了 binlog 日誌用於歸檔,沒有 crash-safe 的能力。InnoDB 引擎以插件的形式引入 MySQL 時,爲了能夠實現 crash-safe 的能力,引入了 redo log 。

一般我們用 binlog 做主從複製,數據恢復等操作。

binlog 是如何做數據恢復的?

一般我們做數據庫備份是一週一備,一天一備,也可能一月一備。

假設今天中午12點,我們發現部分數據被誤刪了。需要恢復到昨天晚上8點這個時間段。但是數據庫是每天凌晨3點的時候備份,離我們最近的一份備份數據已經缺失,只能恢復到昨天凌晨3點。這個時候我們就可以拿出昨天凌晨3點到晚上8點這個時間段的 binlog,重放到數據缺失前的那個時刻。在把這份數據恢復到線上數據庫去。

3. 更新操作的執行流程

瞭解了 redo log 和 binlog 這兩個日誌的概念,我們再來看看執行器和 InnoDB 引擎在執行這個簡單的 update 語句時的內部流程。

  1. 執行器先找引擎取 ID=2 這一行。如果數據在內存就直接返回,如果不在內存就先從磁盤讀入內存,再返回。
  2. 執行器拿到數據,給這行的 c 值加 1。
  3. 引擎將這行數據的改動更新到內存中,同時將這個更新操作記錄到 redo log 裏面,此時 redo log 處於prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。
  4. 執行器生成這個操作的 binlog,並把 binlog 寫入磁盤。
  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改成 commit 狀態,更新完成。

下圖出自《MySQL實戰45講》,淺色框表示是在 InnoDB 內部執行的,深色框表示實在執行器中執行的。

4. redo log 和 binlog 的兩階段提交

爲什麼需要兩階段提交?

我們先假設沒有兩階段提交時,可能會有以下兩種情況:

  1. redo log 提交成功了,這時候數據庫掛掉導致 binlog 沒有成功寫入。數據庫重啓之後通過 redo log 把數據恢復回來,但是 binlog 沒有成功寫入,導致我們在做主從複製或者數據恢復的時候,數據不一致。
  2. binlog 提交成功了,這時候數據庫掛掉導致 redo log 沒有成功寫入。數據庫重啓之後,無法恢復崩潰之前提交的那個事務,這部分數據更改在主庫缺失。但是 binlog 已經成功寫入了,從庫反而有了該事務的改動,導致數據不一致。

綜上我們知道,redo log 和 binlog 必須同時成功或同時失敗,才能保證數據一致性。

兩階段提交是如何保證 redo log 和 binlog 同時成功或同時失敗的?

圖片來自《MySQL實戰45講》

假設已經有了兩階段提交,分析一下以下兩種情況:

  1. 假設在上圖的時刻A,redo log 處於 prepare 之後,寫 binlog 之前,數據庫掛掉了。由於此時 binlog 還沒有寫,redo log 也還沒有提交,所以崩潰恢復後,這個事務會回滾。這時候 binlog 還沒寫,所以也不會傳到備庫。
  2. 假設在上圖的時刻B,寫 binlog 之後,redo log 還沒有 commit 前發生 crash。那麼崩潰恢復時,MySQL 會做以下判斷:
    1. 如果 redo log 裏面的事務是完整的,也就是已經有了 commit 標識,則直接提交;
    2. 如果 redo log 裏面的事務只有完整的 prepare,則判斷對應的事務 binlog 是否存在並完整:
      a. 如果是,則提交事務;
      b. 否則,回滾事務。

那麼 MySQL 是怎麼知道 binlog 是否完整的?

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

  • statement 格式的 binlog,最後會有 COMMIT;
  • row 格式的 binlog,最後會有一個 XID event。

5. change buffer

什麼是 change buffer ?

當需要更新一個數據時,如果數據頁在內存裏就直接更新了,如果數據頁不在內存裏,InnoDB 會將這些更新操作緩存在 change buffer 中,這樣就不需要讀磁盤了。在下次查詢需要訪問到這個數據頁的時候,將數據頁讀入內存,然後執行 change buffer 中與這個頁有關的操作。

如果能夠將更新操作先記錄在 change buffer, 減少讀磁盤,更新操作變快。而且數據讀入內存是需要佔用 buffer pool 的,所以這種方式還能夠避免佔用內存,提高內存利用率。

change buffer 是可以持久化的數據,change buffer 在內存中有拷貝,也會被寫入到磁盤中。

將 change buffer 中的操作應用到原數據頁,得到最新結果的過程稱爲 merge。以下情況會觸發 merge:

  1. 訪問數據頁
  2. 系統有後臺線程定期 merge
  3. 數據庫正常關閉也會觸發 merge

爲什麼普通索引比唯一索引效率高?

  1. 查詢時:
    1. 普通索引查出數據頁,數據頁讀入內存,判斷是否有相等的數據,返回數據。
    2. 唯一索引查出數據頁,數據頁讀入內存,直接返回數據。
    3. 雖然普通索引多了一步判斷,但是數據是以頁爲單位讀入內存的,判斷大概率是內存操作,消耗很小,可以忽略。
  2. 更新時:
    1. 普通索引直接更新內存或者緩存到 change buffer 中,結束。
    2. 唯一索引更新時需要判斷是否有數據衝突,所以無法利用 change buffer,當數據頁不在內存時,必須讀磁盤寫入內存再做判斷,效率低於普通索引。

什麼情況下不適合使用 change buffer?

如果某個業務更新後馬上做查詢,即使我們把更新先記錄在 change buffer,讀取操作也會馬上把數據讀入內存,而且立即觸發 merge 操作。這種情況下,隨機訪問磁盤的次數沒有減少,反而增加了 change buffer 的維護代價。所以對於這種業務,change buffer 反而起到了反作用。

6. change buffer 和 redo log

插入時

  1. 插入的數據頁剛好在內存中,直接更新內存中的數據頁(上圖1)。
  2. 數據頁不在內存中,在 change buffer 裏記錄下對該數據頁的改動(上圖2)。
  3. 將上述兩個動作記入 redo log 中(上圖3,4)。

我們可以看到,執行這條語句的成本很低,寫了兩處內存(內存和change buffer),寫了一處磁盤(redo log,兩次操作合在一起寫磁盤),而且還是順序寫(直接寫日誌文件)。

同時,圖中兩個虛線箭頭,是後臺操作(異步操作,空閒時間就刷的那種),不影響該語句的響應時間。

查詢時

  1. 數據在內存時,直接讀取。
  2. 數據不在內存時,從磁盤讀入內存,然後應用 change buffer 裏的操作日誌,在內存生成一個最新的數據。

比較

從上面兩個案例我們可以看出:

  1. redo log 主要節省的是隨機寫磁盤的 IO 消耗(把更新時的隨機寫磁盤轉成順序寫)。
  2. change buffer 主要節省的是隨機讀磁盤的 IO 消耗(減少更新時讀磁盤的次數)。

7. binlog 和 redo log 的持久化

binlog 的寫入機制

binlog 的寫入邏輯:事務執行過程中,先把日誌寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 文件中。

一個事務的 binlog 是不能被拆開寫的,因此不論這個事務多大,也要確保一次性寫入。

系統給 binlog cache 分配了一片內存,每個線程一個,參數 binlog_cache_size 用於控制單個線程內 binlog cache 所佔內存的大小。如果超過了這個參數規定的大小,就要暫存的磁盤中。

事務提交時,執行器把 binlog cache 裏的完整事務寫到 binlog file 和 磁盤中,並清空 binlog cache。狀態如下圖所示:

  1. 圖中的 write,指的是日誌寫入到文件系統的 page cache,並沒有把數據持久化到磁盤,速度比較快。
  2. 圖中的 fsync,指的是日誌最終持久化到磁盤,速度慢。
  3. write 和 fsync 的時機,由參數 sync_binlog 控制:
    1. sync_binlog=0 時,表示每次提交事務都只 write,不 fsync;
    2. sync_binlog=1 時,表示每次提交事務都會執行 fsync;
    3. sync_binlog=N(N>1) 時,表示每次提交事務都 write,但累積 N 個事務後才 fsync。
  4. 將 sync_binlog 設置爲 N,對應的風險是:如果主機發生異常重啓,會丟失最近 N 個事務的 binlog 日誌(沒有持久化到磁盤,主機掛了就丟失了)。

redo log 的寫入機制

  1. 事務在執行過程中,生成的 redo log 會先寫到 redo log buffer 中。
  2. 寫入到 page cache 的速度也很快,寫入到磁盤的速度慢。
  3. innodb_flush_log_at_trx_commit 參數用來控制 redo log 的寫入策略:
    1. 設爲 0 時,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中;
    2. 設爲 1 時,表示每次事務提交時都將 redo log 直接持久化到磁盤;
    3. 設爲 2 時,表示每次事務提交時都只是把 redo log 寫到 page cache。
  4. InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,然後調用 fsync 持久化到磁盤。
  5. 事務執行過程中寫入 redo log buffer 的記錄,也會隨着其他事務的提交或者定時寫入過程持久化到磁盤中。也就是說有些還未提交的事務的 redo log 也會被持久化。
  6. redo log buffer 佔用的空間即將達到 innodb_log_buffer_size 一半的時候,也會觸發持久化操作。

分組提交

爲了降低寫磁盤的次數,redo log 把 write 和 fsync 拆成兩個步驟,當有併發時,事務A寫完 page cahce,事務B也寫完了 page cache,事務A觸發 fsync 的時候,會把兩個事務的 redo log 並在一組,一起寫磁盤。

並且爲了能讓更多的事務加入同一個組,InnoDB 讓 redo log 和 binlog 的 write 和 fsync 交替執行,分組提交的優化,redo log 和 binlog 都有。

WAL 機制是減少磁盤寫,但每次提交事務都要寫 redo log 和 binlog,寫磁盤的次數好像沒有減少?

  1. redo log 和 binlog 都是順序寫,磁盤的順序寫比隨機寫速度快;(日誌寫磁盤都是順序寫的,事務提交後直接把數據寫磁盤就是隨機訪問);
  2. 組提交機制可以大幅降低磁盤的 IOPS 消耗。

MySQL 是如何保證 crash-safe 的。

redo log 是如何保證 crash-safe 的。
寫到 redo log buffer 不能保證 crash-safe,寫到 fs cache 也不能保證 crash-safe,只有 redo log 寫入磁盤之後,數據庫異常重啓,從磁盤中的 redo log 拿出未執行的日誌進行恢復,纔算是 crash-safe。
這也說明了多個事務提交之後才寫磁盤,還是會有事務丟失。只有每個事務提交後都進行寫磁盤才能保證數據完全不丟失。

binlog 爲什麼無法保證 crash-safe?

  1. 如果 binlog 寫入成功了,數據還沒寫入磁盤,數據庫異常崩潰,重啓後主庫沒有這部分數據,而通過 binlog 同步的從庫卻有了這部分配置,導致主從數據不一致。
  2. 如果 數據寫入磁盤,binlog 寫入失敗了,數據庫異常崩潰,重啓後主庫有這部分數據,而通過 binlog 同步的從庫沒有這部分數據,導致主從數據不一致。

能否只使用 binlog 或 redo log 單個日誌保證 crash-safe?

我的理解是:並不是單個 log 無法保證 crash-safe,而是 binlog 本身無法保證 crash-safe,因爲 InnoDB 無法重新設計 binlog,所以引入了 redo log。並且花了很大力氣來保證 redo log 和 binlog 的一致性。
如果重新設計 MySQL,可以使用 redo log 實現 binlog 的功能,也可以把 binlog 設計成 crash-safe 的,這樣就只需要一種 log 了。

參考

02 | 日誌系統:一條SQL更新語句是如何執行的?-極客時間

09 | 普通索引和唯一索引,應該怎麼選擇?-極客時間

12 | 爲什麼我的MySQL會"抖"一下?-極客時間

15 | 答疑文章(一):日誌和索引相關問題-極客時間

23 | MySQL是怎麼保證數據不丟的?-極客時間

本文首發於我的個人博客 https://chaohang.top

作者 張小超

公衆號【超超不會飛】

轉載請註明出處

歡迎關注我的微信公衆號 【超超不會飛】,獲取第一時間的更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章