MySQL 崩潰恢復過程分析

ITPUB社群發表於2022-11-29

天有不測風雲,資料庫有旦夕禍福。

資料庫正常執行時,Redo 日誌就是個累贅。

現在,終於到了 Redo 日誌揚眉吐氣,大顯身手的時候了。

本文我們一起來看看,MySQL 在崩潰恢復過程中都幹了哪些事情,Redo 日誌又是怎麼大顯身手的。

本文介紹的崩潰恢復過程,包含 server 層InnoDB,不涉及其它儲存引擎,內容基於 MySQL 8.0.29 原始碼。

目錄


  • 1. 概述

  • 2. 讀取兩次寫頁面

  • 3. 恢復資料頁

    • 3.1 找到 last_checkpoint_lsn

    • 3.2 修復損壞的資料頁

    • 3.3 讀取 Redo 日誌

    • 3.4 應用 Redo 日誌

  • 4. 刪除 undo 表空間

  • 5. 初始化事務子系統

  • 6. 重建 undo 表空間

  • 7. 處理事務

    • 7.1 清理已提交事務

    • 7.2 回滾未提交 DDL 事務

    • 7.3 回滾未提交 DML 事務

    • 7.4 處理 PREPARE 事務

  • 8. 總結

  • 9. 相關文章


正文

1. 概述

MySQL 崩潰也是一次關閉過程,只是比正常關閉著急了一些。

正常關閉時,MySQL 會做一系列收尾工作,例如:清理 undo 日誌、合併 change buffer 緩衝區等操作。

具體會進行哪些收尾工作,取決於系統變數 innodb_fast_shutdown 的配置。

崩潰直接就是戛然而止,撂挑子不幹了,還沒來得及進行的那些收尾工作怎麼辦?

那就只能等待下次啟動的時候再幹了,這就是本文要介紹的崩潰恢復過程。

2. 讀取兩次寫頁面

MySQL 一旦崩潰,Redo 日誌就要去拯救世界了(MySQL 就是它的世界),Redo 日誌拯救世界的方式就是把還沒來得及刷盤的髒頁恢復到崩潰之前那一刻的狀態。

雖然 Redo 日誌能夠用來恢復資料頁,但這是有前提條件的:資料頁必須完好無損的狀態。

本文我們把系統表空間、獨立表空間、undo 表空間中的頁統稱為資料頁

如果資料頁剛寫了一半,MySQL 就戛然而止,這個資料頁就損壞了,面對這種情況,Redo 日誌也是巧婦難為無米之炊。

Redo 日誌拯救世界之路就要因為這個問題停滯不前嗎?

那顯示是不能的,這就該輪到兩次寫上場了。

兩次寫的官方名字是 double write,它包含記憶體緩衝區dblwr 檔案兩個部分,InnoDB 髒頁刷盤前,都會先把髒頁寫入記憶體緩衝區,再寫入 dblwr 檔案,功之後才會把髒頁刷盤。

兩次寫透過系統變數 innodb_doublewrite 控制開啟或關閉,本文內容基於該系統變數的預設值 ON,表示開啟兩次寫。

如果髒頁寫入記憶體緩衝區和 dblwr 檔案的程中,MySQL 崩潰了,表空間中對應的資料頁還是完整的,下次啟動時,不需要用兩次寫頁面修復這個資料頁。

如果髒頁刷盤時,MySQL 崩潰了,表空間對應的資料頁損壞了,下次啟動時,應用 Redo 日誌到資料頁之前需要用兩次寫頁面修復這個資料頁。

dblwr 檔案 預設位於 MySQL 資料目錄下:

[csch@csch /usr/local/mysql_8_0_29/data] ls -l | grep dblwr
-rw-r-----    1 csch  staff   192K  8 27 12:04 #ib_16384_0.dblwr
-rw-r-----    1 csch  staff   8.2M  8  1 16:29 #ib_16384_1.dblwr

MySQL 啟動過程中,會把 *.dblwr 檔案中的所有兩次寫頁面載入到兩次寫記憶體緩衝區,並用記憶體緩衝區中的兩次寫頁面修復損壞的資料頁,然後再應用 Redo 日誌到資料頁。

3. 恢復資料頁

應用 Redo 日誌到資料頁(3.4 小節),需要先讀取 Redo 日誌(3.3 小節)。

讀取日誌 Redo 日誌,需要有個起點,起點就是最後一次 checkpoint 的 lsn(3.1 小節)。

應用 Redo 日誌有一個前提:資料頁必須是完好無損的。要保證資料頁的完整性,應用 Redo 日誌之前需要修復損壞的資料頁(3.2 小節)。

修復損壞資料頁只需要保證在應用 Redo 日誌之前就行了,之所以安排在 3.2 小節,是遵循了原始碼中的順序。

瞭解本節安排內容順序的邏輯,有助於理解應用 Redo 日誌恢復資料頁的過程,接下來我們正式進入下一個環節。

3.1 找到 last_checkpoint_lsn

讀取 Redo 日誌之前,必須先確定一個起點,這個起點就是 InnoDB 最後一次 checkpoint 操作的 lsn,也就是 last_checkpoint_lsn

每個 Redo 日誌檔案的前 4 個 block 都是保留空間,不會用來寫 Redo 日誌,last_checkpoint_lsn 和其它 checkpoint 資訊一起,位於第 1 個 Redo 日誌檔案的第 2、4 個 block 中。

Redo 日誌檔案中每個 block 的大小為 512 位元組。

InnoDB 每次進行 checkpoint 操作時,都會把 checkpoint_no 加 1,用於標識一次 checkpoint 操作。

然後把本次 checkpoint 資訊寫入 Redo 日誌檔案的第 2 或第 4 個 block 中。具體寫入哪個 block,取決於 checkpoint_no。

如果 checkpoint_no 是奇數,checkpoint 資訊寫入第 4 個 block。

如果 checkpoint_no 是偶數,checkpoint 資訊寫入第 2 個 block。

確定讀取 Redo 日誌的起點時,從第 2、4 個 block 中讀取較大的那個 last_checkpoint_lsn 作為起點。

為什麼 checkpoint 資訊要儲存到 2 個 block 中?

這是一個用於保證 checkpoint 資訊保安性的簡單好用的方法,因為每次 checkpoint 只會往其中一個 block 寫入資訊。

萬一就在某次寫 checkpoint 資訊的過程中 MySQL 崩潰了,有可能導致正在寫入的這個 block 中的 checkpoint 資訊不正確。

這種情況下,另一個 block 中的 checkpoint 資訊肯定是正確的了,因為它裡面的資訊是上一次正常寫入的。

能夠用這種冗餘方式來保證 checkpoint block 的安全性,基於一個前提:last_checkpoint_lsn 不需要那麼精確。

last_checkpoint_lsn 比實際需要應用 Redo 日誌起點處的 lsn 小是沒關係的,不會造成資料頁不正確,只是會多掃描一點 Redo 日誌而已,應用 Redo 日誌時會過濾已經刷盤的髒頁對應的 Redo 日誌。

3.2 修復損壞的資料頁

把兩次寫檔案中的所有資料頁都載入到記憶體緩衝區之後,需要用這些頁來把系統表空間、獨立表空間、undo 表空間中損壞的資料頁恢復到正常狀態。

正常狀態指的是 MySQL 崩潰之前,資料頁最後一次正確的重新整理到磁碟的狀態。

恢復資料頁的過程是對兩次寫記憶體緩衝區中的所有資料頁進行迴圈,從兩次寫資料頁中讀取表空間 ID、頁號,然後根據表空間 ID 和頁號去系統表空間、獨立表空間、undo 表空間中讀取對應的資料頁

讀取到對應的資料頁之後,會根據其 File Header、File Trailer 中的一些欄位判斷資料頁是不是已經損壞了:

首先,從 File Header 中讀取 FILE_PAGE_LSN 欄位,如果 FILE_PAGE_LSN 欄位值大於當前系統已經生成的 Redo 日誌的最大 LSN,說明資料庫出現了不可描述的錯誤,資料頁已經損壞。

然後,從 File Header 中讀取 FILE_PAGE_SPACE_OR_CHECKSUM 欄位值,從 File Trailer 的前 4 位元組中讀取 checksum。

如果 FILE_PAGE_SPACE_OR_CHECKSUM 欄位值和 File Trailer checksum 不一樣,說明資料頁已經損壞。

一旦出現了上面 2 種情況中的 1 種,把兩次寫資料頁的內容複製到對應的資料頁中,資料頁就會恢復到正常狀態了。

3.2 讀取 Redo 日誌

前面確定了讀取 Redo 日誌的起點 last_checkpoint_lsn,接下來就該讀取 Redo 日誌了,主要流程如下:

MySQL 崩潰恢復過程分析

第 1 步,InnoDB 會以 64K 為單位,從 Redo 日誌檔案讀取日誌到 log buffer 中。

64K = 4 * innodb_page_size,所以,每次從 Redo 日誌檔案讀取的資料量取決於系統變數 innodb_page_size。

第 2 步,已經讀取到 log buffer 中的 block,利用 block header 和 block tailer 中的資訊對 block 進行完整性檢驗之後,把 block body 資訊複製到另一個緩衝區 parsing buffer

parsing buffer 是一個 2M 的固定大小緩衝區,用於存放即將要被解析的 Redo 日誌。

Redo 日誌每個 block 的大小為 512 位元組,block header 為 12 位元組,block trailer 為 4 位元組。
從 log buffer 的每個 block 中複製到 parsing buffer 的 block body 大小就是 512-12-4 = 496 位元組,也就是每個 block 中存放的 Redo 日誌資料部分。

第 3 步,解析 parsing buffer 中的 Redo 日誌。

這一步解析 Redo 日誌,實際上只是個預處理操作,並不會完整的解析每一條 Redo 日誌,而是隻會解析每一條 Redo 日誌中的頭資訊以及資料地址,包括以 4 個部分:

  • Redo 日誌型別
  • Redo 日誌所屬資料頁的表空間 ID
  • Redo 日誌所屬資料頁的頁號
  • Redo 日誌資料,這部分只是得到了每一條 Redo 日誌在 block body 中的地址,後面應用 Redo 日誌到資料頁時會用到。

第 4 步,把第 3 步解析出來的每一條 Redo 日誌的 4 個部分都複製到 hash 表中。

MySQL 崩潰恢復過程分析

這個 hash 表是個巢狀結構,第 1 層 hash key 是表空間 ID,value 也是個 hash 結構,也就是第 2 層。

同一個表空間的 Redo 日誌以頁單位組織到一起,存放到以表空間 ID 為 key 的第 1 層 hash value 中。

第 2 層的 hash key 是頁號,value 是需要應用到這個資料頁的 Redo 日誌組成的連結串列。

同一個資料頁的 Redo 日誌連結串列以頁號為 key,放在第 2 層 hash value 中。

連結串列中的 Redo 日誌按照產生的先後順序排列,第 1 條就是要應用的這些 Redo 日誌中最早產生的那條。

第 5 步,應用 Redo 日誌到資料頁。

如果第 4 步進行的過程中,Redo 日誌資料複製到 hash 表之後,導致 hash 表佔用的空間大於 max_memory,那麼需要應用 Redo 日誌到資料頁,應用完成之後,清空 hash 表,為下一批 Redo 日誌資料騰出空間。

這裡的 max_memory 表示 hash 表能夠使用的最大記憶體空間。

1 ~ 5 步是個迴圈執行過程,經過 N 輪迴圈之後,hash 表中有非常大的可能性還存在著最後一批 Redo 日誌,因為佔用空間小於等於 max_memory 而只能在那裡苦苦等待著被應用到 Redo 日誌,這個工作就要等待第 6 步去幹了。

第 6 步,收尾工作。

1 ~ 5 步迴圈結束之後,收尾工作就把 hash 表中剩下的 Redo 日誌應用到資料頁,這是崩潰過程中最後一次應用 Redo 日誌。

前面都沒有提到過存放 Redo 日誌的 hash 表在哪裡,能使用多大記憶體,不知道你有沒有好奇過?

這個 hash 表並不會單獨申請一大塊記憶體,而是借用了 buffer pool 中的記憶體。

因為在崩潰恢復過程中,進行到讀取 Redo 日誌階段時,buffer pool 還沒有真正開始用,所以可以先借來給 hash 表用一下。

不過 hash 表並不能使用 buffer pool 的全部記憶體,而是需要保留一部分記憶體,用於應用 Redo 日誌到資料頁的過程中,載入資料到 buffer pool 中。

保留記憶體大小為:buffer pool 例項數量 * 256 個資料頁,buffer pool 中的剩餘記憶體,就是第 5 步提到的 max_memory,也就是 hash 表能夠使用的最大記憶體。

3.4 應用 Redo 日誌

前面介紹讀取 Redo 日誌,為了流程的完整性,有 2 個步驟已經涉及到應用 Redo 日誌了。這裡要介紹的是應用 Redo 日誌的過程,會比上一小節深入一些。

讀取 Redo 日誌階段,已經把所有需要應用的 Redo 日誌都進行過預處理,並複製到 hash 表了。

存放 Redo 日誌的 hash 表是一個巢狀結構:

  • 第 1 層的 hash key 是表空間 ID,hash value 還是一個 hash 表。
  • 第 2 層的 hash key 是頁號,hash value 是個 Redo 日誌連結串列,連結串列中的每個元素就是一條需要應用的 Redo 日誌,按照產生的先後排序。

把每個資料頁的 Redo 日誌彙總到一起再去應用 Redo 日誌,這樣做的好處是效率高。

在崩潰恢復過程中,每個資料頁只需要被載入到 buffer pool 中一次,一個資料頁的 Redo 日誌能夠一次性應用,乾脆利落。

應用 Redo 日誌就是迴圈這個巢狀的 hash 表,把每一條 Redo 日誌都應用到資料頁中,主要流程如下:

MySQL 崩潰恢復過程分析

第 1 步,從第 1 層 hash 表中取到表空間 ID 和這個 undo 表空間下需要應用的 Redo 日誌組成的第 2 層 hash 表。

第 2 步,從第 2 層 hash 表中取到一個頁號和該資料頁中需要應用的 Redo 日誌連結串列。

第 3 步,判斷當前迴圈的資料頁是不是已經載入到 buffer pool 中了。

如果當前頁沒有載入到 buffer pool 中,進入第 4 步。

如果當前頁已經載入到 buffer pool 中,進入第 5 步。

第 4 步,把不在 buffer pool 中的資料頁載入到 buffer pool 中。

載入資料頁到 buffer pool 中,是一個非同步批次操作,有可能會一次載入多個資料頁。

也就是說,把資料頁從表空間載入到 buffer pool 中會觸發預讀,提前把一批需要應用 Redo 日誌的資料頁一次性載入到 buffer pool 中。

預讀的資料頁,不是隨機讀取的,而是根據第 3 步判斷不在 buffer pool 中的資料頁的頁號(記為 page_no),計算出一個頁號範圍,把這個範圍內需要應用 Redo 日誌的資料頁,全都載入到 buffer pool 中。

頁號範圍的起點:low_limit = page_no - page % 32,終點:low_limit + 32。

迴圈 low_limit ~ low_limit + 32 範圍內的頁號,只要碰到需要應用 Redo 日誌的資料頁,就先把頁號臨時存放到一個陣列裡。

迴圈結束後,把陣列裡的頁號對應的資料頁非同步批次載入到 buffer pool 中。

從上面的邏輯可以看到,一次預讀最多隻讀 32 個資料頁。

第 5 步,應用 Redo 日誌到資料頁。

根據第 1 步取到的表空間 ID和第 2 步取到的頁號,從 hash 表中獲取該資料頁需要應用的 Redo 日誌連結串列。

從資料頁的 File Header 中讀取 FILE_PAGE_LSN,迴圈 Redo 日誌連結串列中的每一條日誌,判斷該日誌的 start_lsn 是否大於等於 FILE_PAGE_LSN。

如果 start_lsn < FILE_PAGE_LSN,說明該 Redo 日誌對應的操作修改的資料頁,在 MySQL 崩潰之前就已經刷盤,該 Redo 日誌就不需要應用到資料頁了。

如果 start_lsn >= FILE_PAGE_LSN,說明該 Redo 日誌需要應用到資料頁。

然後,根據 Redo 日誌型別,呼叫不同的方法解析 Redo 日誌,直接修改 buffer pool 中的資料頁,對該資料頁應用 Redo 日誌的過程就完成了。

1 ~ 5 步是個迴圈過程,直到所有 undo 表空間的 Redo 日誌都被應用到資料頁,迴圈過程結束。

4. 刪除 undo 表空間

MySQL 執行過程中,如果有大事務往 undo 表空間中寫入大量 undo 日誌,undo 表空間會變大。

在早期版本中,undo 表空間變大之後,就不能再縮回去了。

現在,如果系統變數 innodb_undo_log_truncate 設定為 on,當 undo 表空間增長到 innodb_max_undo_log_size 設定的大小(預設值為 1G)之後,InnoDB 會把這個 undo 表空間截斷為初始大小(16M)。

除了透過系統變數控制 undo 表空間自動截斷之外,還可以用下面這個 SQL 手動觸發:

ALTER UNDO TABLESPACE tablespace_name
SET INACTIVE

不管自動還是手動,有可能 InnoDB 正在進行 undo 表空間截斷操作,MySQL 就突然崩潰了,截斷表空間操作還沒有完成,那怎麼辦?

等到下次啟動的時候,InnoDB 需要把未完成的 undo 表空間截斷操作繼續完成。

InnoDB 怎麼知道哪些 undo 表空間的截斷操作沒有完成?

這就需要用到一個標記檔案了,InnoDB 對某個 undo 表空間進行截斷操作之前,會建立一個對應的標記檔案,檔名是這樣的:undo_表空間編號_trunc.log

解釋一下表空間的兩個標識:表空間編號是給我們們人類看的,表空間 ID 是 MySQL 內部使用的,這兩者不一樣。

以 undo_001 表空間為例,表空間編號為 1,InnoDB 對 undo_001 表空間進行截斷操作之前,會建立一個 undo_1_trunc.log 檔案,如下:

[csch@csch /usr/local/mysql_8_0_29/data] ls -l | grep undo
-rw-r-----    1 csch  staff    16M  8 27 12:04 undo_001
-rw-r-----    1 csch  staff    16M  8 27 12:04 undo_002
-rw-r--r--    1 csch  staff    16K  6 22 12:36 undo_1_trunc.log

崩潰恢復過程中,InnoDB 如果發現某個表空間存在對應的 trunc.log 檔案,說明這個 undo 表空間在 MySQL 崩潰時正在進行截斷操作。

但是,只透過 trunc.log 檔案存在這一個條件,並不能確定 undo 表空間截斷操作沒有完成,還要進一步判斷。

接著讀取 trunc.log 檔案的內容,把讀到的內容轉換成數字,判斷這個數字是不是等於 76845412

76845412 是什麼?稍候介紹。

如果等於,說明在 MySQL 崩潰之前,undo 表空間截斷操作已經完成,只是 trunc.log 檔案還沒來得及刪除。此時,直接刪除這個檔案就可以了。

如果不等於,說明 MySQL 崩潰時,undo 表空間截斷操作還沒有完成,那就需要繼續完成。此時,直接刪除 undo 表空間檔案。

被刪除的 undo 表空間要等到初始化事務子系統之後,才會重建,重建過程我們稍後介紹。

舉個例子:啟動過程中發現了 undo_001 表空間對應的 trunc.log 檔案,並且檔案中儲存的數字不是 76845412,那就直接刪除 undo_001 表空間。

刪除之後,就只有 undo_1_trunc.log 檔案能證明 undo_001 表空間存在過了,就像下面這樣:

[csch@csch /usr/local/mysql_8_0_29/data] ls -l | grep undo
-rw-r-----    1 csch  staff    16M  8 27 12:04 undo_002
-rw-r--r--    1 csch  staff    16K  6 22 12:36 undo_1_trunc.log

為什麼這裡不把 undo 表空間對應的 trunc.log 檔案一起刪除?

因為 undo 表空間要等到初始化事務子系統完成之後再重建,而 trunc.log 是 undo 表空間重建的憑證,所以,現在還不能刪除。

接下來我們再看看 trunc.log 檔案的建立和寫入過程。

InnoDB 進行 undo 表空間截斷操作之前,就會建立 trunc.log 檔案(大小為 innodb_page_size 位元組),並把檔案內容的所有位元組都初始化為 NULL,然後開始進行 undo 表空間截斷操作。

操作完成之後,會往 trunc.log 檔案中寫入一個被稱為魔數的數字:76845412,用於標識 undo 表空間截斷操作已經完成。

如果魔數成功寫入 trunc.log 檔案,接下來會把 trunc.log 檔案刪除,undo 表空間的截斷操作就結束了。

5. 初始化事務子系統

現在,我們來到了初始化事務子系統階段。

InnoDB 之所以把初始化事務子系統安排在刪除 undo 表空間之後,有可能是為了避免讀取要被刪除的 undo 表空間,能夠節省一點點時間。

刪除還沒有完成截斷操作的 undo 表空間檔案之後,剩下的 undo 表空間檔案都需要讀取。

從 undo 表空間檔案讀取未完成的事務,初始化事務子系統,主要過程如下:

初始化事務子系統還包含其它操作,不在本文介紹的範圍內。

MySQL 崩潰恢復過程分析

第 1 步,從記憶體中的 undo 表空間物件陣列中讀取 undo 表空間資訊。

undo 表空間預設為 2 個,最多可以有 127 個。

有了獨立 undo 表空間之後,位於系統表空間中的回滾段就已經不再使用了,所以不需要從系統表空間的回滾段中讀取事務資訊。

第 2 步,從 undo 表空間中頁號 = 3 的資料頁中讀取回滾段。

每個 undo 表空間可以有 1 ~ 128 個回滾段,由系統變數 innodb_rollback_segments 控制,預設值為 2.

第 3 步,從回滾段中讀取 undo slot。

回滾段的段頭頁中有 1024 個 undo slot(4 位元組),每個 undo slot 對應一個 undo 段。

如果 undo slot 的值 等於 FIL_NULL,表示這個 undo slot 沒有關聯到 undo 段,繼續執行第 3 步,讀取下一個 undo slot。

如果 undo slot 的值 不等於 FIL_NULL,表示這個 undo slot 關聯了 undo 段,進入第 4 步。

第 4 步,從 undo slot 對應的 undo 段中讀取未完成事務的資訊。

此時,undo slot 的值就是 undo 段的段頭頁頁號,透過這個頁號可以讀取到 undo 段中的事務資訊。

undo slot 關聯了 undo 段,說明資料庫崩潰時,undo 段中的事務還沒有完成,事務狀態可能是以下 3 種之一:

  • TRX_STATE_ACTIVE,表示事務還沒有進入提交階段。
  • TRX_STATE_PREPARED,表示事務已經提交了,但是隻完成了二階段提交的 PREPARE 階段,還沒有完成 COMMIT 階段。
  • TRX_STATE_COMMITTED_IN_MEMORY,表示事務已經完成了二階段提交的 2 個階段,還剩一些收尾工作沒做,這種狀態的事務修改的資料已經可以被其它事務看見了。

    事務的收尾工作有哪些?清理已提交事務小節會介紹。

第 1 ~ 4 步是個迴圈的過程,直到讀完所有 undo 表空間中的事務資訊結束。

6. 重建 undo 表空間

對於存在 trunc.log 檔案的 undo 表空間,因為之前 undo 表空間檔案被刪除了,現在要開始著手重建 undo 表空間了,主要流程如下:

MySQL 崩潰恢復過程分析

第 1 步,建立 trunc.log 檔案,標記 undo 表空間重建操作正在進行中。

看到這裡你可能會奇怪,undo 表空間對應的 trunc.log 檔案不是沒有刪除嗎?這裡為什麼又要建立一次?

別急,且往下看。

在建立 undo 表空間對應的 trunc.log 檔案之前,會先刪除之前舊的 trunc.log 檔案,然後建立新的 trunc.log 檔案。

新舊 trunc.log 檔名是一樣的,例如:對於 undo_001 表空間來說,新舊 trunc.log 檔名都是 undo_1_trunc.log。

為什麼要刪除舊的 trunc.log 檔案再建立新的同名 trunc.log 檔案呢?

因為重建 undo 表空間和新建 undo 表空間是同一套邏輯,而新建 undo 表空間之前,該表空間並不存在對應的 trunc.log 檔案。

為了保持統一的邏輯,所以會先刪除已經存在的 trunc.log 檔案。

第 2 步,建立 undo 表空間檔案,初始大小為 16M,這個大小是硬編碼的。

第 3 步,初始化 undo 表空間,把表空間 ID、各種連結串列資訊寫入表空間的 0 號頁中,然後分配一個新的資料頁,建立並初始化回滾段,回滾段數量由系統變數 innodb_rollback_segments 控制。

第 4 步,迴圈 undo 表空間中的所有回滾段,把每個回滾段中的 1024 個 undo slot 都初始化為 FIL_NULL

第 5 步,標記 undo 表空間重建操作已經完成。

InnoDB 會先往 trunc.log 檔案中寫入一個魔數 76845412,表示重建表空間操作已經完成。

寫入魔數成功之後,再把 trunc.log 檔案刪除,重建一個 undo 表空間的過程就結束了。

如果有多個 undo 表空間需要重建,對於每個 undo 表空間都需要進行 1 ~ 5 步的流程。

7. 處理事務

初始化事務子系統小節,我們介紹過,從 undo 表空間中讀取出來的事務有 3 種狀態:

  • TRX_STATE_ACTIVE
  • TRX_STATE_PREPARED
  • TRX_STATE_COMMITTED_IN_MEMORY

處理事務階段對這 3 種狀態會進行不同的處理,請接著往下看。

7.1 清理已提交事務

這裡要清理的已提交事務,指的是狀態為 TRX_STATE_COMMITTED_IN_MEMORY 的事務,包含 DDL 和 DML 事務。

這種狀態的事務已經完成二階段提交的 PREPARE 和 COMMIT 階段,是已經提交成功的事務,只差最後一點點清理工作,它們修改的資料已經被其它事務看見了。

清理工作主要有幾點:

  • 處理 insert undo 段。
    如果 insert undo 段被快取,undo 段會被加入 insert_undo_cached 連結串列尾部,以備重複使用;
    如果 insert undo 段不能被快取,undo 段就會被釋放。
  • 把事務從讀寫事務連結串列中刪除。
  • 把事務狀態修改為 TRX_STATE_NOT_STARTED

7.2 回滾未提交 DDL 事務

未提交事務指的是狀態為 TRX_STATE_ACTIVE 的事務,也就是活躍事務。

崩潰恢復過程中,這種狀態的事務是需要直接回滾的。

你可能會有個疑問,DDL 事務不是不能回滾嗎?

DDL 事務不能回滾,這只是針對 MySQL 使用者而言,MySQL 內部並不會受到這個限制。

我們在使用 MySQL 的過程中,如果在一個 DML 事務中間執行了一條 DDL 語句,會觸發隱式提交,直接把 DML 事務提交了。

然後 DDL 會開啟一個新事務,這個新事務是自動提交的,DDL 執行完成之後,事務就直接提交了,我們是沒有機會對 DDL 事務進行回滾操作的。

MySQL 沒給我們回滾 DDL 事務的機會,但是它自己有這個特權。

7.3 回滾未提交 DML 事務

未提交的 DDL 事務和 DML 事務在原始碼中是在不同時間觸發的,它的回滾過程和 DDL 事務一樣。

事務回滾的過程比較複雜,本文我們就不展開說了,後續會寫一篇文章專門介紹事務回滾的過程。

7.4 處理 PREPARE 事務

PREPARE 事務指的是狀態為 TRX_STATE_PREPARED 的事務,這種狀態的事務比較特殊,在崩潰恢復過程中,既有可能被提交,也有可能被回滾。

PREPARE 事務提交還是回滾,取決於這個事務的 XID 是否已經寫入到 binlog 日誌檔案中。

事務 XID 是以 binlog event 的方式寫入 binlog 日誌檔案的,event 的名字是 XID_EVENT

一個事務只會有一個 XID,也就只會有一個 XID_EVENT 了。

要知道事務的 XID_EVENT 是否已經寫入到 binlog 日誌檔案,需要先讀取 binlog 日誌檔案。

從上面的介紹可以看到,處理 PREPARE 事務依賴於 binlog 日誌檔案,因此,這部分邏輯是在開啟 binlog 日誌檔案的過程中實現的。

MySQL 在同一時刻只會往一個 binlog 日誌檔案中寫入  binlog event,在崩潰那一刻,承載寫入 event 的檔案是最後一個 binlog 日誌檔案。

因此,崩潰恢復過程中,只需要掃描最後一個 binlog 日誌檔案,找到其中所有的 XID_EVENT, 用於判斷 PREPARE 事務的 XID_EVENT 是否已經寫入 binlog 日誌檔案。

如果 MySQL 上一次是正常關閉,啟動過程中,不會存在沒有完成的事務,沒有 PREPARE 事務需要處理,也就不用掃描最後一個 binlog 日誌檔案了。

MySQL 怎麼知道上一次是不是正常關閉呢?

每個 binlog 日誌檔案的第 1 個 EVENT 都是 FORMAT_DESCRIPTION_EVENT,用於描述 binlog 日誌檔案格式資訊,這個 EVENT 中包含一個標記 LOG_EVENT_BINLOG_IN_USE_F

binlog 日誌檔案建立時,這個標記位會被設定為 1,表示 binlog 日誌檔案正在被使用。

LOG_EVENT_BINLOG_IN_USE_F 標記在 2 種情況下會被清除:

  • 切換 binlog 日誌檔案時,舊 binlog 日誌檔案的 LOG_EVENT_BINLOG_IN_USE_F 標記會被清除。
  • MySQL 正常關閉時,正在使用的 binlog 日誌檔案的 LOG_EVENT_BINLOG_IN_USE_F 標記會被清除。

如果 MySQL 突然崩潰,來不及把這個標記設定為 0。

那麼下次啟動時,MySQL 讀取最後一個 binlog 日誌檔案的 FORMAT_DESCRIPTION_EVENT 發現 LOG_EVENT_BINLOG_IN_USE_F 標記為 1,就會進入處理 PREPARE 事務階段,主要流程如下:

MySQL 崩潰恢復過程分析

第 1 步,掃描最後一個 binlog 日誌檔案,讀取 EVENT,找到其中所有的 XID_EVENT,並把讀取到的事務 XID 存放到一個集合中。

第 2 步,InnoDB 迴圈讀寫事務連結串列,每找到一個 PREPARE 事務都存放到陣列中,最後把陣列返回給 server 層。

第 3 步,讀取 InnoDB 返回的 PREPARE 事務陣列,判斷事務 XID 是否在第 1 步的事務 XID 集合中。

第 4 步,提交或回滾事務。

如果事務 XID 集合中,說明 MySQL 崩潰之前,事務 XID_EVENT 就已經寫入 binlog 日誌檔案了。

XID_EVENT 有可能已經同步給從伺服器,從伺服器上可能已經重放了這個事務。

這種情況下,為了保證主從資料的一致性,事務在主伺服器上也需要提交

如果事務 XID 不在集合中,說明 MySQL 崩潰之前,事務 XID_EVENT 沒有寫入 binlog 日誌檔案。

XID_EVENT 肯定也就沒有同步給從伺服器了,同樣為了保證主從資料的一致性,事務在主伺服器上也不能提交,而是需要回滾

3 ~ 4 步是個迴圈過程,迴圈完 InnoDB 返回的 PREPARE 事務陣列之後,處理 PREPARE 事務的過程結束,崩潰恢復主要流程也就完成了。

8. 總結

MySQL 崩潰恢復過程的核心工作有 2 點:

  • 對於 MySQL 崩潰之前還沒有重新整理到磁碟的資料頁(也就是髒頁),用 Redo 日誌把這些資料頁恢復到 MySQL 崩潰之前那一刻的狀態,這相當於對髒頁進行一次刷盤操作。

    在這之前,需要用兩次寫緩衝區中的頁把損壞的資料頁修復為正常狀態,然後才能在此基礎上用 Redo 日誌恢復資料頁。

  • 清理、提交、回滾還沒有完成的事務。

    對於已完成二階段提交的 PREPARE、COMMIT 2 個階段的事務,做收尾工作。

    對於活躍狀態的事務,直接回滾。

    對於 PREPARE 狀態的事務,如果事務 XID 已寫入 binlog 日誌檔案,提交事務,否則回滾事務。

9. 相關文章

  • Redo 日誌從產生到寫入日誌檔案
  • Undo 日誌用什麼儲存結構支援無鎖併發寫入?
  • MySQL 事務二階段提交

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2925716/,如需轉載,請註明出處,否則將追究法律責任。

相關文章