tft每日頭條

 > 生活

 > mysql故障分析腳本

mysql故障分析腳本

生活 更新时间:2024-07-27 19:48:11

天有不測風雲,數據庫有旦夕禍福。

前面寫 Redo 日志的文章介紹過,數據庫正常運行時,Redo 日志就是個累贅。

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

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

本文介紹的崩潰恢複過程,包含 server 層和 innodb,不涉及其它存儲引擎,内容基于 MySQL 8.0.29 源碼。

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

第 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故障分析腳本(崩潰恢複過程分析)2

這個 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故障分析腳本(崩潰恢複過程分析)3

第 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故障分析腳本(崩潰恢複過程分析)4

第 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故障分析腳本(崩潰恢複過程分析)5

第 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故障分析腳本(崩潰恢複過程分析)6

第 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 日志文件,提交事務,否則回滾事務。
,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved