MySQL系列2:InnoDB儲存引擎

2023-09-15 06:00:22

1. 架構回顧

上一篇我們講解了MySQL的邏輯架構,重新回顧一下,用一張新的圖來認識一下該架構。

整體架構分為service層與儲存引擎層,請求交給連線池後,由後臺執行緒處理,並將請求轉發給SQL介面,隨後交給解析器執行,如果解析器發現命中快取,直接從快取讀資料返回,如果沒有,依次往下執行,直到從儲存引擎再到磁碟或者記憶體(儲存引擎對應的快取中)查詢結果返回。

2. 三種紀錄檔

在聊儲存引擎前,不得不聊三種紀錄檔,undo log、redo log、binlog,因為儲存引擎的執行過程中時刻跟寫紀錄檔與刷盤有關係。

2.1 undo log

undo log是做回滾用的,記錄了某一次資料更新或者修改的逆向操作,比如現需要修改記錄,將a=1更新成a=2,undo log就記錄執行逆向操作,將a=2更新成a=1,再比如將某一條資料刪除,undo log就記錄改資料的恢復操作,insert該資料,保證資料操作不成功,通過undo log能恢復到修改前的版本。

2.2 redo log

redo log主要是保證資料修改不丟失。該紀錄檔屬於儲存引擎層,屬於物理紀錄檔,記錄修改了哪些資料。有了 redo log,InnoDB 就可以保證即使資料庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為 crash-safe。

2.3 binlog

binlog屬於service層,記錄了sql功能上做的操作,屬於邏輯紀錄檔,比如執行了什麼樣的sql更新語句等。主要用來做歷史資料恢復與主從同步。

2.4 binlog與redo log區別

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

3. InnoDB的記憶體結構Buffer Pool以及執行流程

InnoDB儲存引擎中有一個非常重要的放在記憶體裡的元件,就是緩衝池(Buffer Pool),這裡面會快取很多的資料,以便於以後在查詢的時候,萬一你要是記憶體緩衝池裡有資料,就可以不用去查磁碟了。我們先要明確一點事實,就是所有的操作都是基於Buffer Pool進行操作,而不是磁碟,因為Buffer Pool記憶體操作速度快。舉個例子,引擎要更新「id=10」這一行資料 ,先看一下下圖的執行流程。

  1. 現將該資料從磁碟載入到快取;
  2. 將資料的逆向操作記錄到undo log紀錄檔,儲存舊值,未來如果事務未提交可以執行回滾,恢復原始資料;
  3. 在Buffer Pool中更新資料;
  4. 將Redo Log寫入Redo Log Buffer;
  5. 執行Redo Log的第一個階段,也叫prepare準備階段,將Redo Log紀錄檔刷到對應的磁碟檔案;
  6. 執行binlog刷盤,將binlog紀錄檔也刷到對應的磁碟檔案;
  7. 執行Redo Log的第二個階段,也叫commit提交階段,對應的binlog檔名稱和這次更新的binlog紀錄檔在檔案裡的位置,都寫入到redo log紀錄檔檔案裡去,同時在redo log紀錄檔檔案裡寫入一個commit標記。在完成這個事情之後,才算最終完成了事務的提交。

上面流程還涉及其他細節,比如刷盤策略,為何要兩階段等,將在下面一一展開。

3.1 Redo Log刷盤策略

redo log有三種刷盤策略,該策略是通過innodb_flush_log_at_trx_commit來設定的,0-不刷磁碟,1-刷磁碟(建議),2-刷os cache,下圖分析如果刷os cache,預設1s以後才能刷到磁碟,期間宕機會導致資料丟失,如下圖。

如果設定不刷盤,Buffer Pool清空後資料也一樣丟失,所以建議設定引數為1。如果上述刷盤不成功,第一階段事務就沒成功,後續binlog就根本不會執行,整個事務都會回滾,相當於更新白做。

3.2 binlog刷盤策略

sync_binlog引數可以控制binlog的刷盤策略,他的預設值是0,此時你把binlog寫入磁碟的時候,其實不是直接進入磁碟檔案,而是進入os cache記憶體快取。所以跟之前分析的一樣,如果此時機器宕機,那麼你在os cache裡的binlog紀錄檔是會丟失的,我們看下圖的示意。

如果要是把sync_binlog引數設定為1的話,那麼此時會強制在提交事務的時候,把binlog直接寫入到磁碟檔案裡去,那麼這樣提交事務之後,哪怕機器宕機,磁碟上的binlog是不會丟失的。

3.2 Redo Log的兩階段提交

當我們把binlog寫入磁碟檔案之後,接著就會完成最終的事務提交,此時會把本次更新對應的binlog檔名稱和這次更新的binlog紀錄檔在檔案裡的位置,都寫入到redo log紀錄檔檔案裡去,同時在redo log紀錄檔檔案裡寫入一個commit標記。在完成這個事情之後,才算最終完成了事務的提交,我們看下圖的示意

最後一步在redo紀錄檔中寫入commit標記的意義是什麼?說白了,他其實是用來保持redo log紀錄檔與binlog紀錄檔一致的。我們來舉個例子,假設我們在提交事務的時候,一共有上圖中的5、6、7三個步驟,必須是三個步驟都執行完畢,才算是提交了事務。那麼在我們剛完成步驟5的時候,也就是redo log剛刷入磁碟檔案的時候,mysql宕機了,此時怎麼辦?這個時候因為沒有最終的事務commit標記在redo紀錄檔裡,所以此次事務可以判定為不成功。不會說redo紀錄檔檔案裡有這次更新的紀錄檔,但是binlog紀錄檔檔案裡沒有這次更新的紀錄檔,不會出現資料不一致的問題。

如果要是完成步驟6的時候,也就是binlog寫入磁碟了,此時mysql宕機了,怎麼辦?同理,因為沒有redo log中的最終commit標記,因此此時事務提交也是失敗的。

必須是在redo log中寫入最終的事務commit標記了,然後此時事務提交成功,而且redo log裡有本次更新對應的紀錄檔,binlog裡也有本次更新對應的紀錄檔 ,redo log和binlog完全是一致的。

下面有圖來展示一下這個兩階段提交的過程

prepare 階段:將 XID(內部 XA 事務的 ID) 寫入到 redo log,同時將 redo log 對應的事務狀態設定為 prepare,然後將 redo log 持久化到磁碟;
commit 階段:把 XID 寫入到 binlog,然後將 binlog 持久化到磁碟,接著呼叫引擎的提交事務介面,將 redo log 狀態設定為 commit,此時該狀態並不需要持久化到磁碟,只需要 write 到檔案系統的 page cache 中就夠了,因為只要 binlog 寫磁碟成功,就算 redo log 的狀態還是 prepare 也沒有關係,一樣會被認為事務已經執行成功。

通過這種兩階段提交的方案,就能夠確保redo-log、bin-log兩者的紀錄檔資料是相同的。

3.3 後臺IO執行緒隨機將記憶體更新後的髒資料刷回磁碟

現在我們假設已經提交事務了,此時一次更新「update users set name='xxx' where id=1」,他已經把記憶體裡的buffer pool中的快取資料更新了,同時磁碟裡有redo紀錄檔和binlog紀錄檔,都記錄了把我們指定的「id=1」這行資料修改了「name='xxx'」。此時我們會思考一個問題了,但是這個時候磁碟上的資料檔案裡的「id=1」這行資料的name欄位還是等於舊的值啊!所以MySQL有一個後臺的IO執行緒,會在之後某個時間裡,隨機的把記憶體buffer pool中的修改後的髒資料給刷回到磁碟上的資料檔案裡去,我們看下圖:

當上圖中的執行緒把buffer pool裡的修改後的髒資料刷回磁碟的之後,磁碟上的資料才會跟記憶體裡一樣,都是name=xxx這個修改以後的值了!在你執行緒把髒資料刷回磁碟之前,哪怕mysql宕機崩潰也沒關係,因為重啟之後,會根據redo紀錄檔恢復之前提交事務做過的修改到記憶體裡去,就是id=1的資料的name修改為了xxx,然後等適當時機,執行緒自然還是會把這個修改後的資料刷到磁碟上的資料檔案裡去的。

4 總結

大家通過一次更新資料的流程,就可以清晰地看到,InnoDB儲存引擎主要就是包含了一些buffer pool、redo log buffer等記憶體裡的快取資料,同時還包含了一些undo紀錄檔檔案,redo紀錄檔檔案等東西,同時mysql server自己還有binlog紀錄檔檔案。在執行更新的時候,每條SQL語句,都會對應修改buffer pool裡的快取資料、寫undo紀錄檔、寫redo log buffer幾個步驟;但是當你提交事務的時候,一定會把redo log刷入磁碟,binlog刷入磁碟,完成redo log中的事務commit標記;最後後臺的IO執行緒會隨機的把buffer pool裡的髒資料刷入磁碟裡去。