MySQL - 宕機時數據不丟失的原理


總結 TODO

 

問題

在開始閱讀本文之前,可以先思考一下下面兩個問題。

眾所周知,MySQL 有四大特性:ACID,其中 D 指的是持久性(Durability),它的含義是 MySQL 的事務一旦提交,它對數據庫的改變是永久性的,即數據不會丟失,那么 MySQL 究竟是如何實現的呢?
MySQL 數據庫所在服務器宕機或者斷電后,會出現數據丟失的問題嗎?如果不丟失,它又是如何來實現數據不丟失的呢?
在 MySQL 5.5 以后,默認的存儲引擎為 InnoDB,且只有 InnoDB 引擎支持事務和數據崩潰恢復,因此本文所有內容均是基於 InnoDB 存儲引擎為前提。

redo log

MySQL 在更新數據時,為了減少磁盤的隨機 IO,因此並不會直接更新磁盤上的數據,而是先更新 Buffer Pool 中緩存頁的數據,等到合適的時間點,再將這個緩存頁持久化到磁盤。而 Buffer Pool 中所有緩存頁都是處於內存當中的,當 MySQL 宕機或者機器斷電,內存中的數據就會丟失,因此 MySQL 為了防止緩存頁中的數據在更新后出現數據丟失的現象,引入了 redo log 機制。

當進行增刪改操作時,MySQL 會在更新 Buffer Pool 中的緩存頁數據時,會記錄一條對應操作的 redo log 日志,這樣如果出現 MySQL 宕機或者斷電時,如果有緩存頁的數據還沒來得及刷入磁盤,那么當 MySQL 重新啟動時,可以根據 redo log 日志文件,進行數據重做,將數據恢復到宕機或者斷電前的狀態,保證了更新的數據不丟失,因此 redo log 又叫做重做日志。它的本質是保證事務提交后,更新的數據不丟失。

與 binlog 不同的是,redo log 中記錄的是物理日志,是 InnoDB 引擎記錄的,而 binlog 記錄的是邏輯日志,是 MySQL 的 Server 層記錄的。什么意思呢?binlog 中記錄的是 SQL 語句(實際上並不一定為 SQL 語句,這與 binlog 的格式有關,如果指定的是 STATEMENT 格式,那么 binlog 中記錄的就是 SQL 語句),也就是邏輯日志;而 redo log 中則記錄的是對磁盤上的某個表空間的某個數據頁的某一行數據的某個字段做了修改,修改后的值為多少,它記錄的是對物理磁盤上數據的修改,因此稱之為物理日志。

redo log 日志文件是持久化在磁盤上的,磁盤上可以有多個 redo log 文件,MySQL 默認有 2 個 redo log 文件,每個文件大小為 48MB,這兩個文件默認存放在 MySQL 數據目錄的文件夾下,這兩個文件分別為 ib_logfile0 和 ib_logfile1。(本人電腦上安裝的 MySQL 時,指定存放數據的目錄是:/usr/local/mysql/data,因此這兩個 redo log 文件所在的磁盤路徑分別是:/usr/local/mysql/data/ib_logfile0 和/usr/local/mysql/data/ib_logfile1)。可以通過如下命令來查看 redo log 文件相關的配置。

 

 


查詢結果如圖。

 

 

 

  • innodb_log_files_in_group 表示的是有幾個 redo log 日志文件。
  • innodb_log_file_size 表示的是每個 redo log 日志文件的大小為多大。
  • innodb_log_group_home_dir 表示的是 redo log 文件存放的目錄,在這里./表示的是相對於 MySQL 存放數據的目錄,這些參數可以根據實際需要自定義修改。

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 刷入到磁盤中。

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

  1. MySQL 正常關閉的時候;
  2. MySQL 的后台線程每隔一段時間定時的講 redo log buffer 刷入到磁盤,默認是每隔 1s 刷一次;
  3. 當 redo log buffer 中的日志寫入量超過 redo log buffer 內存的一半時,即超過 8MB 時,會觸發 redo log buffer 的刷盤;
  4. 當事務提交時,根據配置的參數 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 中刷新到磁盤上)

如何保證數據不丟失

前面介紹了 redo log 相關的基礎知識,下面來看下 MySQL 究竟是如何來保證數據不丟失的。

  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 標識,那么就不會進行數據重做,磁盤上數據還是原來的數據,也就是事務沒有提交,這符合我們的邏輯。

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

如果配置成 0,則 redo log 即使標記為 commit 狀態了,由於此時 redo log 處於 redo log buffer 中,如果斷電,redo log buffer 內存中的數據會丟失,此時如果恰好 buffer pool 中的臟頁也還沒有刷新到磁盤,而 redo log 也丟失了,所以在 MySQL 重啟后,由於丟失了一條 redo log,因此就會丟失一條 redo log 對應的重做日志,這樣斷電前提交的那一次事務的數據也就丟失了。

如果配置成 2,則事務提交時,會將 redo log buffer(實際上是此次事務所對應的那條 redo log 所在的 redo log block )寫入磁盤,但是操作系統通常都會存在 os cache,所以這時候的寫只是將數據寫入到了 os cache,如果機器斷電,數據依然會丟失。

而如果配置成 1,則表示事務提交時,就將對應的 redo log block 寫入到磁盤,同時調用 fsync,fsync 會將數據強制從 os cache 中刷入到磁盤中,因此數據不會丟失。

從效率上來說,0 的效率最高,因為不涉及到磁盤 IO,但是會丟失數據;而 1 的效率最低,但是最安全,不會丟失數據。2 的效率居中,會丟失數據。在實際的生產環境中,通常要求是的是“雙 1 配置”,即將 innodb_flush_log_at_trx_commit 設置為 1,另外一個 1 指的是寫 binlog 時,將 sync_binlog 設置為 1,這樣 binlog 的數據就不會丟失(后面的文章中會分析 binlog 相關的內容)。

疑惑

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

首先引入 redo log 機制是十分必要的。因為寫 redo log 時,我們將 redo log 日志追加到文件末尾,雖然也是一次磁盤 IO,但是這是順序寫操作(不需要移動磁頭);而對於直接將數據更新到磁盤,涉及到的操作是將 buffer pool 中緩存頁寫入到磁盤上的數據頁上,由於涉及到尋找數據頁在磁盤的哪個地方,這個操作發生的是隨機寫操作(需要移動磁頭),相比於順序寫操作,磁盤的隨機寫操作性能消耗更大,花費的時間更長,因此 redo log 機制更優,能提升 MySQL 的性能。

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

總結

最后解答下文章開頭的兩個問題。

  • MySQL 通過 redo log 機制,以及兩階段事務提交(prepare 和 commit)來保證了事務的持久性。
  • MySQL 中,只有當 innodb_flush_log_at_trx_commit 參數設置為 1 時,才不會出現數據丟失情況,當設置為 0 或者 2 時,可能會出現數據丟失。

————————————————
版權聲明:本文為CSDN博主「天堂2013」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_34436819/article/details/105664256


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM