Photo 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 語句時的內部流程。
- 執行器先找引擎取 ID=2 這一行。如果數據在內存就直接返回,如果不在內存就先從磁盤讀入內存,再返回。
- 執行器拿到數據,給這行的 c 值加 1。
- 引擎將這行數據的改動更新到內存中,同時將這個更新操作記錄到 redo log 裏面,此時 redo log 處於prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。
- 執行器生成這個操作的 binlog,並把 binlog 寫入磁盤。
- 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改成 commit 狀態,更新完成。
下圖出自《MySQL實戰45講》,淺色框表示是在 InnoDB 內部執行的,深色框表示實在執行器中執行的。
4. redo log 和 binlog 的兩階段提交
爲什麼需要兩階段提交?
我們先假設沒有兩階段提交時,可能會有以下兩種情況:
- redo log 提交成功了,這時候數據庫掛掉導致 binlog 沒有成功寫入。數據庫重啓之後通過 redo log 把數據恢復回來,但是 binlog 沒有成功寫入,導致我們在做主從複製或者數據恢復的時候,數據不一致。
- binlog 提交成功了,這時候數據庫掛掉導致 redo log 沒有成功寫入。數據庫重啓之後,無法恢復崩潰之前提交的那個事務,這部分數據更改在主庫缺失。但是 binlog 已經成功寫入了,從庫反而有了該事務的改動,導致數據不一致。
綜上我們知道,redo log 和 binlog 必須同時成功或同時失敗,才能保證數據一致性。
兩階段提交是如何保證 redo log 和 binlog 同時成功或同時失敗的?
假設已經有了兩階段提交,分析一下以下兩種情況:
- 假設在上圖的時刻A,redo log 處於 prepare 之後,寫 binlog 之前,數據庫掛掉了。由於此時 binlog 還沒有寫,redo log 也還沒有提交,所以崩潰恢復後,這個事務會回滾。這時候 binlog 還沒寫,所以也不會傳到備庫。
- 假設在上圖的時刻B,寫 binlog 之後,redo log 還沒有 commit 前發生 crash。那麼崩潰恢復時,MySQL 會做以下判斷:
- 如果 redo log 裏面的事務是完整的,也就是已經有了 commit 標識,則直接提交;
- 如果 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:
- 訪問數據頁
- 系統有後臺線程定期 merge
- 數據庫正常關閉也會觸發 merge
爲什麼普通索引比唯一索引效率高?
- 查詢時:
- 普通索引查出數據頁,數據頁讀入內存,判斷是否有相等的數據,返回數據。
- 唯一索引查出數據頁,數據頁讀入內存,直接返回數據。
- 雖然普通索引多了一步判斷,但是數據是以頁爲單位讀入內存的,判斷大概率是內存操作,消耗很小,可以忽略。
- 更新時:
- 普通索引直接更新內存或者緩存到 change buffer 中,結束。
- 唯一索引更新時需要判斷是否有數據衝突,所以無法利用 change buffer,當數據頁不在內存時,必須讀磁盤寫入內存再做判斷,效率低於普通索引。
什麼情況下不適合使用 change buffer?
如果某個業務更新後馬上做查詢,即使我們把更新先記錄在 change buffer,讀取操作也會馬上把數據讀入內存,而且立即觸發 merge 操作。這種情況下,隨機訪問磁盤的次數沒有減少,反而增加了 change buffer 的維護代價。所以對於這種業務,change buffer 反而起到了反作用。
6. change buffer 和 redo log
插入時
- 插入的數據頁剛好在內存中,直接更新內存中的數據頁(上圖1)。
- 數據頁不在內存中,在 change buffer 裏記錄下對該數據頁的改動(上圖2)。
- 將上述兩個動作記入 redo log 中(上圖3,4)。
我們可以看到,執行這條語句的成本很低,寫了兩處內存(內存和change buffer),寫了一處磁盤(redo log,兩次操作合在一起寫磁盤),而且還是順序寫(直接寫日誌文件)。
同時,圖中兩個虛線箭頭,是後臺操作(異步操作,空閒時間就刷的那種),不影響該語句的響應時間。
查詢時
- 數據在內存時,直接讀取。
- 數據不在內存時,從磁盤讀入內存,然後應用 change buffer 裏的操作日誌,在內存生成一個最新的數據。
比較
從上面兩個案例我們可以看出:
- redo log 主要節省的是隨機寫磁盤的 IO 消耗(把更新時的隨機寫磁盤轉成順序寫)。
- 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。狀態如下圖所示:
- 圖中的 write,指的是日誌寫入到文件系統的 page cache,並沒有把數據持久化到磁盤,速度比較快。
- 圖中的 fsync,指的是日誌最終持久化到磁盤,速度慢。
- write 和 fsync 的時機,由參數 sync_binlog 控制:
- sync_binlog=0 時,表示每次提交事務都只 write,不 fsync;
- sync_binlog=1 時,表示每次提交事務都會執行 fsync;
- sync_binlog=N(N>1) 時,表示每次提交事務都 write,但累積 N 個事務後才 fsync。
- 將 sync_binlog 設置爲 N,對應的風險是:如果主機發生異常重啓,會丟失最近 N 個事務的 binlog 日誌(沒有持久化到磁盤,主機掛了就丟失了)。
redo log 的寫入機制
- 事務在執行過程中,生成的 redo log 會先寫到 redo log buffer 中。
- 寫入到 page cache 的速度也很快,寫入到磁盤的速度慢。
innodb_flush_log_at_trx_commit
參數用來控制 redo log 的寫入策略:- 設爲 0 時,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中;
- 設爲 1 時,表示每次事務提交時都將 redo log 直接持久化到磁盤;
- 設爲 2 時,表示每次事務提交時都只是把 redo log 寫到 page cache。
- InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,然後調用 fsync 持久化到磁盤。
- 事務執行過程中寫入 redo log buffer 的記錄,也會隨着其他事務的提交或者定時寫入過程持久化到磁盤中。也就是說有些還未提交的事務的 redo log 也會被持久化。
- 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,寫磁盤的次數好像沒有減少?
- redo log 和 binlog 都是順序寫,磁盤的順序寫比隨機寫速度快;(日誌寫磁盤都是順序寫的,事務提交後直接把數據寫磁盤就是隨機訪問);
- 組提交機制可以大幅降低磁盤的 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?
- 如果 binlog 寫入成功了,數據還沒寫入磁盤,數據庫異常崩潰,重啓後主庫沒有這部分數據,而通過 binlog 同步的從庫卻有了這部分配置,導致主從數據不一致。
- 如果 數據寫入磁盤,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更新語句是如何執行的?-極客時間
本文首發於我的個人博客 https://chaohang.top
作者 張小超
公衆號【超超不會飛】
轉載請註明出處
歡迎關注我的微信公衆號 【超超不會飛】,獲取第一時間的更新。