本篇文章主要介紹 Redo Log 和 Undo Log:
- 利用 Redo Log 和 Undo Log 實作本地事務的原子性、持久性
- Redo Log 的寫回策略
- Redo Log Buffer 的刷盤時機
日志:Redo Log 和 Undo Log · 語雀 (yuque.com)
通過寫入日志來保證原子性、持久性是業界的主流做法,
介紹 Redo Log 和 Undo Log
Redo Log 是什么:Redo Log 被稱為重做日志,
Undo Log 是什么:Undo Log 被稱為撤銷日志、回滾日志,
技術是為了解決問題而生的,通過 Redo Log 我們可以實作崩潰恢復,防止資料更新丟失,保證事務的持久性,也就是說,在機器故障恢復后,系統仍然能夠通過 Redo Log 中的資訊,持久化已經提交的事務的操作結果,
技術是為了解決問題而生的,Undo Log 的作用 / 功能:
- 事務回滾:可以對提前寫入的資料變動進行擦除,實作事務回滾,保證事務的原子性,
- 實作 MVCC 機制:Undo Log 也用于實作 MVCC 機制,存盤記錄的多個版本的 Undo Log,形成版本鏈,
Undo Log 中存盤了回滾需要的資料,在事務回滾或者崩潰恢復時,根據 Undo Log 中的資訊對提前寫入的資料變動進行擦除,
Redo Log 和 Undo Log 都是用于實作事務的特性,并且都是在存盤引擎層實作的,由于只有 InnoDB 存盤引擎支持事務,因此只有使用 InnoDB 存盤引擎的表才會使用 Redo Log 和 Undo Log,
實作本地事務的原子性、持久性
“Write-Ahead Log”日志方案
MySQL 的 InnoDB 存盤引擎使用“Write-Ahead Log”日志方案實作本地事務的原子性、持久性,
“提前寫入”(Write-Ahead),就是在事務提交之前,允許將變動資料寫入磁盤,與“提前寫入”相反的就是,在事務提交之前,不允許將變動資料寫入磁盤,而是等到事務提交之后再寫入,
“提前寫入”的好處是:有利于利用空閑 I/O 資源,但“提前寫入”同時也引入了新的問題:在事務提交之前就有部分變動資料被寫入磁盤,那么如果事務要回滾,或者發生了崩潰,這些提前寫入的變動資料就都成了錯誤,“Write-Ahead Log”日志方案給出的解決辦法是:增加了一種被稱為 Undo Log 的日志,用于進行事務回滾,
變動資料寫入磁盤前,必須先記錄 Undo Log,Undo Log 中存盤了回滾需要的資料,在事務回滾或者崩潰恢復時,根據 Undo Log 中的資訊對提前寫入的資料變動進行擦除,
“Write-Ahead Log”在崩潰恢復時,會經歷以下三個階段:
- 分析階段(Analysis):該階段從最后一次檢查點(Checkpoint,可理解為在這個點之前所有應該持久化的變動都已安全落盤)開始掃描日志,找出所有沒有 End Record 的事務,組成待恢復的事務集合(一般包括 Transaction Table 和 Dirty Page Table),
- 重做階段(Redo):該階段依據分析階段中,產生的待恢復的事務集合來重演歷史(Repeat History),找出所有包含 Commit Record 的日志,將它們寫入磁盤,寫入完成后增加一條 End Record,然后移除出待恢復事務集合,
- 回滾階段(Undo):該階段處理經過分析、重做階段后剩余的待恢復事務集合,此時剩下的都是需要回滾的事務(被稱為 Loser),根據 Undo Log 中的資訊回滾這些事務,
MySQL 中一條 SQL 更新陳述句的執行程序
以下的執行程序限定在,使用 InnoDB 存盤引擎的表
-
事務開始
-
申請加鎖:表鎖、MDL 鎖、行鎖、索引區間鎖(看情況加哪幾種鎖)
-
執行器找存盤引擎取資料,
-
- 如果記錄所在的資料頁本來就在記憶體(innodb_buffer_cache)中,存盤引擎就直接回傳給執行器;
- 否則,存盤引擎需要先將該資料頁從磁盤讀取到記憶體,然后再回傳給執行器,
-
執行器拿到存盤引擎給的行資料,進行更新操作后,再呼叫存盤引擎介面寫入這行新資料,
-
存盤引擎將回滾需要的資料記錄到 Undo Log,并將這個更新操作記錄到 Redo Log,此時 Redo Log 處于 prepare 狀態,并將這行新資料更新到記憶體(innodb_buffer_cache)中,同時,然后告知執行器執行完成了,隨時可以提交事務,
-
手動事務 commit:執行器生成這個操作的 Binary Log,并把 Binary Log 寫入磁盤,
-
執行器呼叫存盤引擎的提交事務介面,存盤引擎把剛剛寫入的 Redo Log 改成 commit 狀態,
-
事務結束
其中第 5 步,將這個更新操作記錄到 Redo Log,生成的 Redo Log 是存盤在 Redo Log Buffer 后就回傳,還是必須寫入磁盤后才能回傳呢?
這就是 Redo Log 的寫入策略,Redo Log 的寫入策略由 innodb_flush_log_at_trx_commit 引數控制,該引數不同的值對應不同的寫入策略,
還有第 6 步,把 Binary Log 寫入磁盤和 Redo Log 一樣,也有相應的寫回策略,由引數 sync_binlog 控制,
通常我們說 MySQL 的“雙 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設定成 1,也就是說,一個事務提交前,需要等待兩次刷盤,一次是 Redo Log 刷盤(prepare 階段),一次是 Binary Log 刷盤,
Redo Log 的兩階段提交 & 崩潰恢復
在上面【MySQL 中一條 SQL 更新陳述句的執行程序】部分,最后將 Redo Log 的寫入拆成了兩個步驟:prepare 和 commit,這就是"兩階段提交",
“兩階段提交”的作用 / 目的:
- 將事務設定為 prepare,為了在崩潰重啟時,能夠知道事務的狀態
- 保證兩份日志(Binary Log 和 Redo Log)之間的邏輯一致,
如果先寫 Redo Log,再寫 Binary Log 或者 先寫 Binary Log,再寫 Redo Log,寫入第一個日志后,如果此時發生了崩潰,那么第二個日志沒有寫入,就造成了兩個日志的不一致,資料庫的狀態就有可能和用 Binary Log 恢復出來的庫的狀態不一致,備庫利用 Binary Log 進行資料同步,就會出現主備庫資料不一致的問題,具體的講解可以看極客時間的專欄《MySQL實戰45講》

而使用“兩階段提交”,遵守“崩潰恢復時,判斷事務該提交、還是該回滾的規則”,就可以保證兩份日志(Binary Log 和 Redo Log)之間的邏輯一致,
“崩潰恢復時,判斷事務該提交、還是該回滾的規則”如下:
- 如果 Redo Log 里面的事務是完整的,也就是已經有了 commit 標識,那么利用該 Redo Log 中的資訊,持久化事務的操作結果;
- 如果 Redo Log 里面的事務只有完整的 prepare,則判斷對應事務的 Binary Log 是否存在并完整:
a. 如果是,利用該 Redo Log 中的資訊,持久化事務的操作結果;
b. 如果否,則回滾事務,根據 Undo Log 中的資訊對提前寫入的資料變動進行擦除,
如果事務寫入 Redo Log 處于 prepare 階段之后、寫 Binary Log 之前,發生了崩潰(也就是時刻 A 發生了崩潰),由于此時 Binary Log 還沒寫,Redo Log 也還沒處于 commit 狀態,所以崩潰恢復的時候,這個事務會回滾,這時 Binary Log 還沒寫,所以也不會傳到備庫,主庫和備庫的資料狀態一致,
如果事務寫入 Binary Log 之后,Redo Log 還沒處于 commit 狀態之前,發生了崩潰(也就是時刻 B 發生了崩潰),根據崩潰恢復時的判斷規則中第 2 條,Redo Log 處于 prepare 階段,Binary Log 完整,所以崩潰恢復的時候,會利用該 Redo Log 中的資訊,持久化事務的操作結果,這時 Binary Log 已經寫了,所以會傳到備庫,主庫和備庫的資料狀態一致,
Binary Log 的寫入在崩潰恢復時,判斷事務該提交還是該回滾時,起到了至關重要的作用,只有 Binary Log 寫入成功才能保證兩份日志(Binary Log 和 Redo Log)之間的邏輯一致,才能考慮提交,
Redo Log 配置的選項
- innodb_log_buffer_size:Redo Log Buffer 的記憶體大小
- innodb_log_file_size:單個 Redo Log 檔案的空間大小
- innodb_log_files_in_group:Redo Log 檔案的數量(默認值是 2)
- innodb_log_group_home_dir:Redo Log 檔案的存盤目錄(默認值是 .\ ,即資料目錄)
- innodb_flush_log_at_trx_commit:Redo Log 的寫入策略
Redo Log 的寫入策略
我們在【MySQL 中一條 SQL 更新陳述句的執行程序】部分的第 5 步中說:存盤引擎將這行新資料更新到記憶體(innodb_buffer_cache)中,同時,將這個更新操作記錄到 Redo Log,此時 Redo Log 處于 prepare 狀態,然后告知執行器執行完成了,隨時可以提交事務,
生成的 Redo Log 是存盤在 Redo Log Buffer 后就回傳,還是必須寫入磁盤后才能回傳呢?這就是 Redo Log 的寫入策略,Redo Log 的寫入策略由 innodb_flush_log_at_trx_commit 引數控制,
我們可以通過修改該引數的值,設定 Redo Log 的寫入策略,該引數可選的值有 3 個:
- 值為 0 :表示每次事務提交時,只是把 Redo Log 存盤到記憶體(Redo Log Buffer)就回傳,不關心寫入檔案
- 值為 1 :表示每次事務提交時,將 Redo Log Buffer 中的內容寫入并同步到檔案后才能回傳(write + fsync 才能回傳,這是引數的默認值)
- 值為 2 :表示每次事務提交時,只是把 Redo Log Buffer 中的內容寫入內核緩沖區,但不對檔案進行同步,何時同步由作業系統來決定(write,fsync 的時機由作業系統決定)
Redo Log 檔案組
MySQL 的資料目錄(使用 show variables like 'datadir' 查看)下默認有兩個名為 ib_logfile0 和
ib_logfile1 的檔案,Redo Log Buffer 中的 Redo Log 默認情況下就是重繪到這兩個磁盤檔案中,
資料目錄的位置也可以通過以下命令查看:select @@datadir;


如果我們對默認的 Redo Log 檔案組不滿意,可以通過下邊幾個啟動引數來調節:
- innodb_log_buffer_size:每個 Redo Log 檔案的空間大小
- innodb_log_file_size:每個 Redo Log 檔案的最大空間大小
- innodb_log_files_in_group:Redo Log 檔案組中所有 Redo Log 檔案的數量(默認值是 2,最大值是 100)
- innodb_log_group_home_dir:Redo Log 檔案的存盤目錄(默認值是 .\ ,即 MySQL 的資料目錄)
從上邊的描述中可以看到,磁盤上的 Redo Log 檔案不只一個,而是以一個 日志檔案組 的形式出現的,這些檔案
以 ib_logfile[數字] ( 數字 可以是 0 、 1 、 2 ...)的形式進行命名,
在將 Redo Log 寫入 日志檔案組 時,是從 ib_logfile0 開始寫,如果 ib_logfile0 寫滿了,就接著 ib_logfile1 寫,同理, ib_logfile1 寫滿了就去寫 ib_logfile2 ,依此類推,
如果寫到最后一個檔案該怎么辦呢?那就重新轉到 ib_logfile0 繼續寫,所以整個程序如下圖所示:

總共的 Redo Log 檔案空間大小其實就是:innodb_log_file_size × innodb_log_files_in_group ,(單個檔案的空間大小 * 檔案組中的檔案個數)
如果采用回圈使用的方式向 Redo Log 檔案組里寫資料的話,那就會造成追尾,也就是后寫入的 Redo Log 覆寫掉前邊寫的 Redo Log,為了解決 Redo Log 的覆寫寫入問題,InnoDB 的設計者提出了 checkpoint 的概念,
Redo Log 寫入 Redo Log Buffer
Redo Log 的格式
InnoDB 的設計者為 Redo Log 定義了多種型別,以應對事務對資料庫的不同修改場景,但是絕大部分型別的 Redo Log 都有下邊這種通用的結構:

各個部分的詳細釋義如下:
- type:該條 Redo Log 的型別
- space ID :表空間ID
- page number :頁號
- data :該條 Redo Log 的具體內容
在 MySQL 5.7.21 這個版本中,InnoDB 的設計者一共為 Redo Log 設計了 53 種不同的型別,各種型別的 Redo Log 的不同之處在于 data 的具體結構不同,
Redo Log 的具體格式可以看掘金小冊《MySQL 是怎樣運行的:從根兒上理解 MySQL》
這些型別的 Redo Log 既包含 物理 層面的意思,也包含 邏輯 層面的意思,具體指:
- 物理層面看,這些日志都指明了對哪個表空間的哪個頁進行了什么修改,
- 邏輯層面看,在系統奔潰重啟時,并不能直接根據 Redo Log 中的資訊,將頁面內的某個偏移量處恢復成某個資料,而是需要呼叫一些事先準備好的函式,執行完這些函式后才可以將頁面恢復成系統奔潰前的樣子,
總結來說,Redo Log 中記錄的是該操作對哪個表空間的哪個頁的哪個偏移量進行了什么修改,
Mini-Transaction
一個事務可能包含多條 SQL 陳述句,每一條 SQL 陳述句可能包含多個「對底層頁面的操作」,每個「對底層頁面的操作」可能包含多個 Redo Log,這樣的一個「對底層頁面的操作」的程序被稱為 Mini-Transaction,簡稱 mtr,
「對底層頁面的操作」比如說:
- 向聚簇索引對應的 B+ 樹的某個頁面中插入一條記錄,插入一條記錄這個操作可能包含多個 Redo Log
我們需要保證一個「對底層頁面的操作」對應的多個 Redo Log 不可分割,即一個「對底層頁面的操作」是原子的,這個操作對應的 Redo Log 要么都寫入磁盤,要么都不寫入磁盤,所以 InnoDB 的設計者規定在執行這些需要保證原子性的操作時必須以 組 的形式來記錄 Redo Log,在進行奔潰恢復時,針對某個組中的 Redo Log,要么把全部的 Redo Log 都恢復掉,要么一個 Redo Log 也不恢復,
那么 InnoDB 的設計者是怎么做到分組的呢?InnoDB 的設計者在一個「對底層頁面的操作」的最后一個 Redo Log 后面加上一個特殊型別的 Redo Log,相當于某個需要保證原子性的操作產生的一系列 Redo Log 必須要以一個特殊型別的 Redo Log 結尾,這樣在奔潰恢復時:
- 只有當決議到特殊型別的 Redo Log 時,才認為決議到了一組完整的 Redo Log,才會進行恢復,
- 否則的話直接放棄前邊決議到的 Redo Log,

Redo Log Buffer
Redo Log Buffer 就是在服務器啟動時,向作業系統申請的大一片連續的記憶體空間,
這片連續的記憶體空間被劃分為若干個連續的用來存盤 Redo Log 的資料頁,
用來存盤 Redo Log 的資料頁被稱為 Redo Log Block,
我們可以通過啟動引數 innodb_log_buffer_size 來指定 Redo Log Buffer 的大小,


- LOG_BLOCK_CHECKPOINT_NO :表示 checkpoint 的序號

Redo Log 寫入 Redo Log Buffer
我們前邊說過一個 mtr 執行程序中可能產生若干個 Redo Log ,這些 Redo Log 是一個不可分割的組,所以其實并不是每生成一個 Redo Log,就將其插入到 Redo Log Buffer 中,而是每個 mtr 運行程序中產生的日志先暫時存到一個地方,當該 mtr 結束的時候,將程序中產生的一組 Redo Log 再全部復制到 Redo Log Buffer 中,
不同的事務可能是并發執行的,所以不同事務的 mtr 可能是交替執行的,每當一個 mtr 執行完成時,伴隨
該 mtr 生成的一組 Redo Log 就需要被復制到 Redo Log Buffer 中,也就是說不同事務的 mtr 可能是交替寫入 Redo Log Buffer 的,
Redo Log Buffer 中 Redo Log 的刷盤時機
mtr 運行程序中產生的一組 Redo Log 在 mtr 結束時會被復制到 Redo Log Buffer 中,在一些情況下 Redo Log Buffer 中的 Redo Log 會被寫回磁盤,Redo Log 的刷盤時機如下:
- Redo Log Buffer 的空間不足時,執行刷盤操作
- 一個事務提交時,執行刷盤操作(需要設定指定引數)
- 將某個臟頁重繪到磁盤前,會先保證該臟頁對應的 Redo Log 重繪到磁盤中(Redo Log 是順序寫入的,因此在將某個臟頁對應的 Redo Log 從 Redo Log Buffer 重繪到磁盤時,也會保證將在其之前產生的 Redo Log 也重繪到磁盤中,
- 后臺執行緒不停的執行刷盤操作
- 正常關閉服務器時,執行刷盤操作
- 做 checkpoint 時,執行刷盤操作
Redo Log Buffer 的空間不足時,執行刷盤操作
Redo Log Buffer 的空間是有限的,空間大小由 innodb_log_buffer_size 來指定,
InnoDB 的設計者認為:如果 Redo Log Buffer 的記憶體被占用 1 / 2,就需要把 Redo Log Buffer 中的 Redo Log 重繪到磁盤,
一個事務提交時,執行刷盤操作
在前面【Redo Log 的寫入策略】部分,講到我們可以通過設定 innodb_flush_log_at_trx_commit 引數的值,在事務提交時執行刷盤操作后才能回傳,
事務提交時執行刷盤操作后才能回傳是 Redo Log 的默認寫入策略,
后臺執行緒不停的執行刷盤操作
后臺有一個執行緒,每秒都會執行一次刷盤操作,后臺執行緒執行刷盤操作的頻率可以通過引數設定,
具體通過哪個引數設定,我也不清楚,
正常關閉服務器時,執行刷盤操作
做 checkpoint 時,執行刷盤操作
等等
Undo Log 的寫回策略
MySQL中的 Undo Log 嚴格的講不是 Log,而是資料,因此它的管理和落盤都跟資料是一樣的:
- Undo 的磁盤結構并不是順序的,而是像資料一樣按 Page 管理
- Undo 寫入時,也像資料一樣產生對應的 Redo Log
- Undo 的 Page 也像資料一樣快取在 Buffer Pool 中,跟資料 Page 一起做 LRU 換入換出,以及刷臟,Undo Page 的刷臟也像資料一樣要等到對應的 Redo Log 落盤之后
之所以這樣實作,首要的原因是 MySQL 中的 Undo Log 不只是承擔 Crash Recovery 時保證 Atomic 的作用,更需要承擔 MVCC 對歷史版本的管理的作用,設計目標是高事務并發,方便的管理和維護,因此當做資料更合適,
但既然還叫 Log,就還是需要有 Undo Log 的責任,那就是保證 Crash Recovery 時,如果看到資料的修改,一定要能看到其對應 Undo 的修改,這樣才有機會通過事務的回滾保證 Crash Atomic,標準的 Undo Log 這一步是靠 WAL 實作的,也就是要求 Undo 寫入先于資料落盤,而 InnoDB 中 Undo Log 作為一種特殊的資料,這一步是通過 redo 的 min-transaction 保證的,簡單的說就是資料的修改和對應的 Undo 修改,他們所對應的 Redo Log 被放到同一個 min-transaction 中,同一個 min-transaction 中的所有 Redo Log 在 Crash Recovery 時以一個整體進行重放,要么全部重放,要么全部丟棄,
作者:CatKang
鏈接:https://www.zhihu.com/question/267595935/answer/2204949497
來源:知乎
Undo Log 配置的選項
- innodb_max_undo_log_size:值為,單個 Undo Log 最大可占用的位元組存盤空間;單位為,位元組(默認值是 1 G),
- innodb_undo_directory:Undo Log 檔案的存盤目錄(默認值是 .\ ,即資料目錄)表示回滾日志的存盤目錄是資料目錄,資料目錄的位置可以通過查詢變數“datadir”來查看,
- innodb_undo_log_encrypt:Undo Log 是否加密(默認值是 off,即不加密),
- innodb_undo_log_truncate:Undo Log 是否自動截斷回收(默認值是 on,即自動截斷回收),這個變數有效的前提是設定了使用獨立表空間,
- innodb_undo_tablespaces:值為 Undo Log 的獨立表空間的數量(默認值是 0,即 Undo Log 沒有獨立的表空間,默認記錄到共享表空間 ibdata 檔案中)

參考資料
20 | 日志(下):系統故障,如何恢復資料? (geekbang.org)
MySQL 是怎樣運行的:從根兒上理解 MySQL - 小孩子4919 - 掘金課程 (juejin.cn)
02 | 日志系統:一條SQL更新陳述句是如何執行的?-極客時間 (geekbang.org)
關于Innodb undo log的重繪時機? - 知乎 (zhihu.com)
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/16672433.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/506066.html
標籤:MySQL
