Java架構直通車——InnoDB事務是如何通過日誌來實現的?

InnoDB的日誌分爲redo log和undo log。

Redo log

redo log叫做重做日誌,是用來實現事務的持久性(用於數據庫的崩潰恢復),當事務提交之後會把所有修改信息都會存到該日誌中。該日誌由兩部分組成,一個是在內存裏的redo log buffer,另一個是在磁盤裏的redo log文件。

mysql 爲了提升性能不會把每次的修改都實時同步到磁盤,而是會先存到Buffer Pool(緩衝池)裏頭,把這個當作緩存來用。然後使用後臺線程去做緩衝池和磁盤之間的同步。

那麼問題來了,如果還沒來的同步的時候宕機或斷電了怎麼辦?由於buffer pool是在內存裏的, 這樣會導致丟部分已提交事務的修改信息!
所以引入了redo log來記錄已成功提交事務的修改信息,之後,系統重啓後讀取redo log恢復最新數據。雖然redo log也有內存buffer緩衝的部分,如果要嚴格保證數據不丟失,就要在事務提交前做一次磁盤寫入,但是這種IO操作相比於buffer pool這種以頁(16kb)爲管理單位的隨機寫入,它做的是幾個字節的順序寫入,效率要高得多。

比如下面的操作,從銀行卡賬戶轉賬到理財賬戶表:
在這裏插入圖片描述

於是我們又引入了一個問題,既然redo log也是分爲內存和磁盤兩個部分,不是也會丟失事務嗎?

redo log buffer會不會丟失事務?

要了解上面這個問題,還要詳細說一說redo log buffer的原理。

當一條 SQL 更新完 Buffer Pool 中的緩存頁後,就會記錄一條 redo log 日誌,前面提到了 redo log 日誌是存儲在磁盤上的,那麼此時是不是立馬就將 redo log 日誌寫入磁盤呢?顯然不是的,而是先寫入一個叫做 redo log buffer 的緩存中,redo log buffer 是一塊不同於 buffer pool 的內存緩存區,在 MySQL 啓動的時候,向內存中申請的一塊內存區域,它是 redo log 日誌緩衝區,默認大小是 16MB,由參數 innodb_log_buffer_size 控制。

redo log buffer 內部又可以劃分爲許多 redo log block,每個 redo log block 大小爲 512 字節。我們寫入的 redo log 日誌,最終實際上是先寫入在 redo log buffer 的 redo log block 中,然後在某一個合適的時間點,將這條 redo log 所在的 redo log block 刷入到磁盤中

這個合適的時間點究竟是什麼時候呢?

  • MySQL 正常關閉的時候;
  • MySQL 的後臺線程每隔一段時間定時的講 redo log buffer 刷入到磁盤,默認是每隔 1s 刷一次;
  • 當 redo log buffer 中的日誌寫入量超過 redo log buffer 內存的一半時,即超過 8MB 時,會觸發 redo log buffer 的刷盤;
  • 當事務提交時,根據配置的參數 innodb_flush_log_at_trx_commit 來決定是否刷盤。
    如果innodb_flush_log_at_trx_commit 參數配置爲 0,表示事務提交時,不進行 redo log buffer 的刷盤操作;
    如果配置爲 1,表示事務提交時,會將此時事務所對應的 redo log 所在的 redo log block 從內存寫入到磁盤,同時調用 fysnc,確保數據落入到磁盤;
    如果配置爲 2,表示只是將日誌寫入到操作系統的緩存,而不進行 fysnc 操作。(進程在向磁盤寫入數據時,是先將數據寫入到操作系統的緩存中:os cache,再調用 fsync 方法,纔會將數據從 os cache 中刷新到磁盤上)

實際上要嚴格保證數據不丟失,必須得保證 innodb_flush_log_at_trx_commit 配置爲 1。

在實際的生產環境中,通常要求是的是“雙 1 配置”,即將 innodb_flush_log_at_trx_commit 設置爲 1,另外一個 1 指的是寫 binlog 時,將 sync_binlog 設置爲 1,這樣 binlog 的數據就不會丟失。


如何保證數據不丟失?

  1. MySQL Server 層的執行器調用 InnoDB 存儲引擎的數據更新接口;
  2. 存儲引擎更新 Buffer Pool 中的緩存頁,
  3. 同時存儲引擎記錄一條 redo log 到 redo log buffer 中,並將該條 redo log 的狀態標記爲 prepare 狀態;
  4. 接着存儲引擎告訴執行器,可以提交事務了。執行器接到通知後,會寫 binlog 日誌,然後提交事務;
  5. 存儲引擎接到提交事務的通知後,將 redo log 的日誌狀態標記爲 commit 狀態;
  6. 接着根據 innodb_flush_log_at_commit 參數的配置,決定是否將 redo log buffer 中的日誌刷入到磁盤。

將 redo log 日誌標記爲 prepare 狀態和 commit 狀態,這種做法稱之爲兩階段事務提交,它能保證事務在提交後,數據不丟失。爲什麼呢?redo log 在進行數據重做時,只有讀到了 commit 標識,纔會認爲這條 redo log 日誌是完整的,纔會進行數據重做,否則會認爲這個 redo log 日誌不完整,不會進行數據重做。

例如,如果在 redo log 處於 prepare 狀態後,buffer pool 中的緩存頁(髒頁)也還沒來得及刷入到磁盤,寫完 biglog 後就出現了宕機或者斷電,此時提交的事務是失敗的,那麼在 MySQL 重啓後,進行數據重做時,在 redo log 日誌中由於該事務的 redo log 日誌沒有 commit 標識,那麼就不會進行數據重做,磁盤上數據還是原來的數據,也就是事務沒有提交,這符合我們的邏輯。

雙“1”設置爲什麼還是要經過磁盤?

既然生產環境一般建議將 innodb_flush_log_at_trx_commit 設置爲 1,也就是說每次更新數據時,最終還是要將 redo log 寫入到磁盤,也就是還是會發生一次磁盤 IO,而我爲什麼不直接停止使用 redo log,而在每次更新數據時,也不要直接更新內存了,直接將數據更新到磁盤,這樣也是發生了一次磁盤 IO,何必引入 redo log 這一機制呢?

首先引入 redo log 機制是十分必要的。因爲寫 redo log 時,我們將 redo log 日誌追加到文件末尾,雖然也是一次磁盤 IO,但是這是順序寫操作(不需要移動磁頭);而對於直接將數據更新到磁盤,這個操作發生的是隨機寫操作(需要移動磁頭做尋址)。

從另一方面來講,通常一次更新操作,我們往往只會涉及到修改幾個字節的數據,而如果因爲僅僅修改幾個字節的數據,就將整個數據頁寫入到磁盤(無論是磁盤還是 buffer pool,他們管理數據的單位都是以頁爲單位),這個代價未免也太了(每個數據頁默認是 16KB),而一條 redo log 日誌的大小可能就只有幾個字節,因此每次磁盤 IO 寫入的數據量更小,那麼耗時也會更短。

Undo log

undo log 叫做回滾日誌,保證事務的原子性,記錄事務修改之前的數據信息,因此假如由於系統錯誤或者rollback操作而回滾的話可以根據undo log的信息來進行回滾到沒被修改前的狀態。

他正好跟前面所說的重做日誌所記錄的相反,重做日誌記錄數據被修改後的信息。undo log主要記錄的是數據的邏輯變化,爲了在發生錯誤時回滾之前的操作,需要將之前的操作都記錄下來,然後在發生錯誤時纔可以回滾。

在這裏插入圖片描述

總結

事務的原子性是通過 undo log 來實現的
事務的持久性性是通過 redo log 來實現的
事務的隔離性是通過 (讀寫鎖+MVCC)來實現的

Undo log是InnoDB MVCC事務特性的重要組成部分。

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