日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
MySQL崩潰恢復(fù)過程分析

天有不測風(fēng)云,數(shù)據(jù)庫有旦夕禍福。

前面寫 Redo 日志的文章介紹過,數(shù)據(jù)庫正常運(yùn)行時(shí),Redo 日志就是個(gè)累贅。

現(xiàn)在,終于到了 Redo 日志揚(yáng)眉吐氣,大顯身手的時(shí)候了。

本文我們一起來看看,MySQL 在崩潰恢復(fù)過程中都干了哪些事情,Redo 日志又是怎么大顯身手的。

本文介紹的崩潰恢復(fù)過程,包含 server 層和 InnoDB,不涉及其它存儲(chǔ)引擎,內(nèi)容基于 MySQL 8.0.29 源碼。

正文

1、概述

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

正常關(guān)閉時(shí),MySQL 會(huì)做一系列收尾工作,例如:清理 undo 日志、合并 change buffer 緩沖區(qū)等操作。

具體會(huì)進(jìn)行哪些收尾工作,取決于系統(tǒng)變量 innodb_fast_shutdown 的配置。

崩潰直接就是戛然而止,撂挑子不干了,還沒來得及進(jìn)行的那些收尾工作怎么辦?

那就只能等待下次啟動(dòng)的時(shí)候再干了,這就是本文要介紹的崩潰恢復(fù)過程。

2、讀取兩次寫頁面

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

雖然 Redo 日志能夠用來恢復(fù)數(shù)據(jù)頁,但這是有前提條件的:數(shù)據(jù)頁必須完好無損的狀態(tài)。

本文我們把系統(tǒng)表空間、獨(dú)立表空間、undo 表空間中的頁統(tǒng)稱為數(shù)據(jù)頁。

如果數(shù)據(jù)頁剛寫了一半,MySQL 就戛然而止,這個(gè)數(shù)據(jù)頁就損壞了,面對(duì)這種情況,Redo 日志也是巧婦難為無米之炊。

Redo 日志拯救世界之路就要因?yàn)檫@個(gè)問題停滯不前嗎?

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

兩次寫?的官方名字是 double write?,它包含內(nèi)存緩沖區(qū)?和 dblwr 文件兩個(gè)部分,InnoDB 臟頁刷盤前,都會(huì)先把臟頁寫入內(nèi)存緩沖區(qū),再寫入 dblwr 文件,成功之后才會(huì)把臟頁刷盤。

兩次寫通過系統(tǒng)變量 innodb_doublewrite? 控制開啟或關(guān)閉,本文內(nèi)容基于該系統(tǒng)變量的默認(rèn)值 ON,表示開啟兩次寫。

如果臟頁寫入內(nèi)存緩沖區(qū)和 dblwr 文件的程中,MySQL 崩潰了,表空間中對(duì)應(yīng)的數(shù)據(jù)頁還是完整的,下次啟動(dòng)時(shí),不需要用兩次寫頁面修復(fù)這個(gè)數(shù)據(jù)頁。

如果臟頁刷盤時(shí),MySQL 崩潰了,表空間對(duì)應(yīng)的數(shù)據(jù)頁損壞了,下次啟動(dòng)時(shí),應(yīng)用 Redo 日志到數(shù)據(jù)頁之前?,需要用兩次寫頁面修復(fù)這個(gè)數(shù)據(jù)頁。

dblwr 文件 默認(rèn)位于 MySQL 數(shù)據(jù)目錄下:

[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 啟動(dòng)過程中,會(huì)把 *.dblwr 文件中的所有兩次寫頁面加載到兩次寫內(nèi)存緩沖區(qū),并用內(nèi)存緩沖區(qū)中的兩次寫頁面修復(fù)損壞的數(shù)據(jù)頁,然后再應(yīng)用 Redo 日志到數(shù)據(jù)頁。

3、恢復(fù)數(shù)據(jù)頁

應(yīng)用 Redo 日志到數(shù)據(jù)頁(3.4 小節(jié)),需要先讀取 Redo 日志(3.3 小節(jié))。

讀取日志 Redo 日志,需要有個(gè)起點(diǎn),起點(diǎn)就是最后一次 checkpoint 的 lsn(3.1 小節(jié))。

應(yīng)用 Redo 日志有一個(gè)前提:數(shù)據(jù)頁必須是完好無損的。要保證數(shù)據(jù)頁的完整性,應(yīng)用 Redo 日志之前需要修復(fù)損壞的數(shù)據(jù)頁(3.2 小節(jié))。

修復(fù)損壞數(shù)據(jù)頁只需要保證在應(yīng)用 Redo 日志之前就行了,之所以安排在 3.2 小節(jié),是遵循了源碼中的順序。

了解本節(jié)安排內(nèi)容順序的邏輯,有助于理解應(yīng)用 Redo 日志恢復(fù)數(shù)據(jù)頁的過程,接下來我們正式進(jìn)入下一個(gè)環(huán)節(jié)。

(1)找到 last_checkpoint_lsn

讀取 Redo 日志之前,必須先確定一個(gè)起點(diǎn),這個(gè)起點(diǎn)就是 InnoDB 最后一次 checkpoint 操作的 lsn,也就是 last_checkpoint_lsn。

每個(gè) Redo 日志文件的前 4 個(gè) block 都是保留空間,不會(huì)用來寫 Redo 日志,last_checkpoint_lsn? 和其它 checkpoint 信息一起,位于第 1 個(gè) Redo 日志文件的第 2、4 個(gè) block 中。

Redo 日志文件中每個(gè) block 的大小為 512 字節(jié)。

InnoDB 每次進(jìn)行 checkpoint 操作時(shí),都會(huì)把 checkpoint_no 加 1,用于標(biāo)識(shí)一次 checkpoint 操作。

然后把本次 checkpoint 信息寫入 Redo 日志文件的第 2 或第 4 個(gè) block 中。具體寫入哪個(gè) block,取決于 checkpoint_no。

如果 checkpoint_no 是奇數(shù),checkpoint 信息寫入第 4 個(gè) block。

如果 checkpoint_no 是偶數(shù),checkpoint 信息寫入第 2 個(gè) block。

確定讀取 Redo 日志的起點(diǎn)時(shí),從第 2、4 個(gè) block 中讀取較大的那個(gè) last_checkpoint_lsn 作為起點(diǎn)。

為什么 checkpoint 信息要存儲(chǔ)到 2 個(gè) block 中?

這是一個(gè)用于保證 checkpoint 信息安全性的簡單好用的方法,因?yàn)槊看?checkpoint 只會(huì)往其中一個(gè) block 寫入信息。

萬一就在某次寫 checkpoint 信息的過程中 MySQL 崩潰了,有可能導(dǎo)致正在寫入的這個(gè) block 中的 checkpoint 信息不正確。

這種情況下,另一個(gè) block 中的 checkpoint 信息肯定是正確的了,因?yàn)樗锩娴男畔⑹巧弦淮握懭氲摹?/p>

能夠用這種冗余方式來保證 checkpoint block 的安全性,基于一個(gè)前提:last_checkpoint_lsn 不需要那么精確。

last_checkpoint_lsn 比實(shí)際需要應(yīng)用 Redo 日志起點(diǎn)處的 lsn 小是沒關(guān)系的,不會(huì)造成數(shù)據(jù)頁不正確,只是會(huì)多掃描一點(diǎn) Redo 日志而已,應(yīng)用 Redo 日志時(shí)會(huì)過濾已經(jīng)刷盤的臟頁對(duì)應(yīng)的 Redo 日志。

(2)修復(fù)損壞的數(shù)據(jù)頁

把兩次寫文件中的所有數(shù)據(jù)頁都加載到內(nèi)存緩沖區(qū)之后,需要用這些頁來把系統(tǒng)表空間、獨(dú)立表空間、undo 表空間中損壞的數(shù)據(jù)頁恢復(fù)到正常狀態(tài)。

正常狀態(tài)指的是 MySQL 崩潰之前,數(shù)據(jù)頁最后一次正確的刷新到磁盤的狀態(tài)。

恢復(fù)數(shù)據(jù)頁的過程是對(duì)兩次寫內(nèi)存緩沖區(qū)中的所有數(shù)據(jù)頁進(jìn)行循環(huán),從兩次寫數(shù)據(jù)頁中讀取表空間 ID、頁號(hào),然后根據(jù)表空間 ID 和頁號(hào)去系統(tǒng)表空間、獨(dú)立表空間、undo 表空間中讀取對(duì)應(yīng)的數(shù)據(jù)頁。

讀取到對(duì)應(yīng)的數(shù)據(jù)頁之后,會(huì)根據(jù)其 File Header、File Trailer 中的一些字段判斷數(shù)據(jù)頁是不是已經(jīng)損壞了:

首先,從 File Header 中讀取 FILE_PAGE_LSN 字段,如果 FILE_PAGE_LSN 字段值大于當(dāng)前系統(tǒng)已經(jīng)生成的 Redo 日志的最大 LSN,說明數(shù)據(jù)庫出現(xiàn)了不可描述的錯(cuò)誤,數(shù)據(jù)頁已經(jīng)損壞。

然后,從 File Header 中讀取 FILE_PAGE_SPACE_OR_CHECKSUM 字段值,從 File Trailer 的前 4 字節(jié)中讀取 checksum。

如果 FILE_PAGE_SPACE_OR_CHECKSUM 字段值和 File Trailer checksum 不一樣,說明數(shù)據(jù)頁已經(jīng)損壞。

一旦出現(xiàn)了上面 2 種情況中的 1 種,把兩次寫數(shù)據(jù)頁的內(nèi)容復(fù)制到對(duì)應(yīng)的數(shù)據(jù)頁中,數(shù)據(jù)頁就會(huì)恢復(fù)到正常狀態(tài)了。

(3)讀取 Redo 日志

前面確定了讀取 Redo 日志的起點(diǎn) last_checkpoint_lsn,接下來就該讀取 Redo 日志了,主要流程如下:

?第 1 步,InnoDB 會(huì)以 64K? 為單位,從 Redo 日志文件讀取日志到 log buffer 中。

64K = 4 * innodb_page_size,所以,每次從 Redo 日志文件讀取的數(shù)據(jù)量取決于系統(tǒng)變量 innodb_page_size。

第 2 步,已經(jīng)讀取到 log buffer 中的 block,利用 block header 和 block tailer 中的信息對(duì) block 進(jìn)行完整性檢驗(yàn)之后,把 block body 信息拷貝到另一個(gè)緩沖區(qū) parsing buffer。

parsing buffer 是一個(gè) 2M 的固定大小緩沖區(qū),用于存放即將要被解析的 Redo 日志。

Redo 日志每個(gè) block 的大小為 512 字節(jié),block header 為 12 字節(jié),block trailer 為 4 字節(jié)。

從 log buffer 的每個(gè) block 中拷貝到 parsing buffer 的 block body 大小就是 512-12-4 = 496 字節(jié),也就是每個(gè) block 中存放的 Redo 日志數(shù)據(jù)部分。

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

這一步解析 Redo 日志,實(shí)際上只是個(gè)預(yù)處理操作,并不會(huì)完整的解析每一條 Redo 日志,而是只會(huì)解析每一條 Redo 日志中的頭信息以及數(shù)據(jù)地址,包括以 4 個(gè)部分:

  • Redo 日志類型。
  • Redo 日志所屬數(shù)據(jù)頁的表空間 ID。
  • Redo 日志所屬數(shù)據(jù)頁的頁號(hào)。
  • Redo 日志數(shù)據(jù),這部分只是得到了每一條 Redo 日志在 block body 中的地址,后面應(yīng)用 Redo 日志到數(shù)據(jù)頁時(shí)會(huì)用到。

第 4 步,把第 3 步解析出來的每一條 Redo 日志的 4 個(gè)部分都拷貝到 hash 表中。

這個(gè) hash 表是個(gè)嵌套結(jié)構(gòu),第 1 層 hash key 是表空間 ID,value 也是個(gè) hash 結(jié)構(gòu),也就是第 2 層。

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

第 2 層的 hash key 是頁號(hào),value 是需要應(yīng)用到這個(gè)數(shù)據(jù)頁的 Redo 日志組成的鏈表。

同一個(gè)數(shù)據(jù)頁的 Redo 日志鏈表以頁號(hào)為 key,放在第 2 層 hash value 中。

鏈表中的 Redo 日志按照產(chǎn)生的先后順序排列,第 1 條就是要應(yīng)用的這些 Redo 日志中最早產(chǎn)生的那條。

第 5 步,應(yīng)用 Redo 日志到數(shù)據(jù)頁。

如果第 4 步進(jìn)行的過程中,Redo 日志數(shù)據(jù)拷貝到 hash 表之后,導(dǎo)致 hash 表占用的空間大于 max_memory,那么需要應(yīng)用 Redo 日志到數(shù)據(jù)頁,應(yīng)用完成之后,清空 hash 表,為下一批 Redo 日志數(shù)據(jù)騰出空間。

這里的 max_memory 表示 hash 表能夠使用的最大內(nèi)存空間。

1 ~ 5 步是個(gè)循環(huán)執(zhí)行過程,經(jīng)過 N 輪循環(huán)之后,hash 表中有非常大的可能性還存在著最后一批 Redo 日志,因?yàn)檎加每臻g??小于等于?? max_memory 而只能在那里苦苦等待著被應(yīng)用到 Redo 日志,這個(gè)工作就要等待第 6 步去干了。

第 6 步,收尾工作。

1 ~ 5 步循環(huán)結(jié)束之后,收尾工作就把 hash 表中剩下的 Redo 日志應(yīng)用到數(shù)據(jù)頁,這是崩潰過程中最后一次應(yīng)用 Redo 日志。

前面都沒有提到過存放 Redo 日志的 hash 表在哪里,能使用多大內(nèi)存,不知道你有沒有好奇過?

這個(gè) hash 表并不會(huì)單獨(dú)申請(qǐng)一大塊內(nèi)存,而是借用了 buffer pool 中的內(nèi)存。

因?yàn)樵诒罎⒒謴?fù)過程中,進(jìn)行到讀取 Redo 日志階段時(shí),buffer pool 還沒有真正開始用,所以可以先借來給 hash 表用一下。

不過 hash 表并不能使用 buffer pool 的全部內(nèi)存,而是需要保留一部分內(nèi)存,用于應(yīng)用 Redo 日志到數(shù)據(jù)頁的過程中,加載數(shù)據(jù)頁到 buffer pool 中。

保留內(nèi)存大小為:buffer pool 實(shí)例數(shù)量 * 256 個(gè)數(shù)據(jù)頁?,buffer pool 中的剩余內(nèi)存,就是第 5 步提到的 max_memory,也就是 hash 表能夠使用的最大內(nèi)存。

(4)應(yīng)用 Redo 日志

前面介紹讀取 Redo 日志,為了流程的完整性,有 2 個(gè)步驟已經(jīng)涉及到應(yīng)用 Redo 日志了。這里要介紹的是應(yīng)用 Redo 日志的過程,會(huì)比上一小節(jié)深入一些。

讀取 Redo 日志階段,已經(jīng)把所有需要應(yīng)用的 Redo 日志都進(jìn)行過預(yù)處理,并拷貝到 hash 表了。

存放 Redo 日志的 hash 表是一個(gè)嵌套結(jié)構(gòu):

  • 第 1 層的 hash key 是表空間 ID,hash value 還是一個(gè) hash 表。
  • 第 2 層的 hash key 是頁號(hào),hash value 是個(gè) Redo 日志鏈表,鏈表中的每個(gè)元素就是一條需要應(yīng)用的 Redo 日志,按照產(chǎn)生的先后排序。

把每個(gè)數(shù)據(jù)頁的 Redo 日志匯總到一起再去應(yīng)用 Redo 日志,這樣做的好處是效率高。

在崩潰恢復(fù)過程中,每個(gè)數(shù)據(jù)頁只需要被加載到 buffer pool 中一次,一個(gè)數(shù)據(jù)頁的 Redo 日志能夠一次性應(yīng)用,干脆利落。

應(yīng)用 Redo 日志就是循環(huán)這個(gè)嵌套的 hash 表,把每一條 Redo 日志都應(yīng)用到數(shù)據(jù)頁中,主要流程如下:

第 1 步,從第 1 層 hash 表中取到表空間 ID 和這個(gè) undo 表空間下需要應(yīng)用的 Redo 日志組成的第 2 層 hash 表。

第 2 步,從第 2 層 hash 表中取到一個(gè)頁號(hào)和該數(shù)據(jù)頁中需要應(yīng)用的 Redo 日志鏈表。

第 3 步,判斷當(dāng)前循環(huán)的數(shù)據(jù)頁是不是已經(jīng)加載到 buffer pool 中了。

如果當(dāng)前頁沒有加載到 buffer pool 中,進(jìn)入第 4 步。

如果當(dāng)前頁已經(jīng)加載到 buffer pool 中,進(jìn)入第 5 步。

第 4 步,把不在 buffer pool 中的數(shù)據(jù)頁加載到 buffer pool 中。

加載數(shù)據(jù)頁到 buffer pool 中,是一個(gè)異步的批量操作,有可能會(huì)一次加載多個(gè)數(shù)據(jù)頁。

也就是說,把數(shù)據(jù)頁從表空間加載到 buffer pool 中會(huì)觸發(fā)預(yù)讀,提前把一批需要應(yīng)用 Redo 日志的數(shù)據(jù)頁一次性加載到 buffer pool 中。

預(yù)讀的數(shù)據(jù)頁,不是隨機(jī)讀取的,而是根據(jù)第 3 步判斷不在 buffer pool 中的數(shù)據(jù)頁的頁號(hào)(記為 page_no),計(jì)算出一個(gè)頁號(hào)范圍,把這個(gè)范圍內(nèi)需要應(yīng)用 Redo 日志的數(shù)據(jù)頁,全都加載到 buffer pool 中。

頁號(hào)范圍的起點(diǎn):low_limit = page_no - page % 32,終點(diǎn):low_limit + 32。

循環(huán) low_limit ~ low_limit + 32 范圍內(nèi)的頁號(hào),只要碰到需要應(yīng)用 Redo 日志的數(shù)據(jù)頁,就先把頁號(hào)臨時(shí)存放到一個(gè)數(shù)組里。

循環(huán)結(jié)束后,把數(shù)組里的頁號(hào)對(duì)應(yīng)的數(shù)據(jù)頁異步批量加載到 buffer pool 中。

從上面的邏輯可以看到,一次預(yù)讀最多只讀 32 個(gè)數(shù)據(jù)頁。

第 5 步,應(yīng)用 Redo 日志到數(shù)據(jù)頁。

根據(jù)第 1 步取到的表空間 ID和第 2 步取到的頁號(hào),從 hash 表中獲取該數(shù)據(jù)頁需要應(yīng)用的 Redo 日志鏈表。

從數(shù)據(jù)頁的 File Header 中讀取 FILE_PAGE_LSN,循環(huán) Redo 日志鏈表中的每一條日志,判斷該日志的 start_lsn 是否大于等于 FILE_PAGE_LSN。

如果 start_lsn < FILE_PAGE_LSN,說明該 Redo 日志對(duì)應(yīng)的操作修改的數(shù)據(jù)頁,在 MySQL 崩潰之前就已經(jīng)刷盤,該 Redo 日志就不需要應(yīng)用到數(shù)據(jù)頁了。

如果 start_lsn >= FILE_PAGE_LSN,說明該 Redo 日志需要應(yīng)用到數(shù)據(jù)頁。

然后,根據(jù) Redo 日志類型,調(diào)用不同的方法解析 Redo 日志,直接修改 buffer pool 中的數(shù)據(jù)頁,對(duì)該數(shù)據(jù)頁應(yīng)用 Redo 日志的過程就完成了。

1 ~ 5 步是個(gè)循環(huán)過程,直到所有 undo 表空間的 Redo 日志都被應(yīng)用到數(shù)據(jù)頁,循環(huán)過程結(jié)束。

4、刪除 undo 表空間

MySQL 運(yùn)行過程中,如果有大事務(wù)往 undo 表空間中寫入大量 undo 日志,undo 表空間會(huì)變大。

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

現(xiàn)在,如果系統(tǒng)變量 innodb_undo_log_truncate 設(shè)置為 on,當(dāng) undo 表空間增長到 innodb_max_undo_log_size 設(shè)置的大小(默認(rèn)值為 1G)之后,InnoDB 會(huì)把這個(gè) undo 表空間截?cái)酁槌跏即笮。?6M)。

除了通過系統(tǒng)變量控制 undo 表空間自動(dòng)截?cái)嘀?,還可以用下面這個(gè) SQL 手動(dòng)觸發(fā):

ALTER UNDO TABLESPACE tablespace_name
SET INACTIVE

不管自動(dòng)還是手動(dòng),有可能 InnoDB 正在進(jìn)行 undo 表空間截?cái)嗖僮鳎琈ySQL 就突然崩潰了,截?cái)啾砜臻g操作還沒有完成,那怎么辦?

等到下次啟動(dòng)的時(shí)候,InnoDB 需要把未完成的 undo 表空間截?cái)嗖僮骼^續(xù)完成。

InnoDB 怎么知道哪些 undo 表空間的截?cái)嗖僮鳑]有完成?

這就需要用到一個(gè)標(biāo)記文件了,InnoDB 對(duì)某個(gè) undo 表空間進(jìn)行截?cái)嗖僮髦埃瑫?huì)創(chuàng)建一個(gè)對(duì)應(yīng)的標(biāo)記文件,文件名是這樣的:undo_表空間編號(hào)_trunc.log。

解釋一下表空間的兩個(gè)標(biāo)識(shí):表空間編號(hào)是給咱們?nèi)祟惪吹模砜臻g ID 是 MySQL 內(nèi)部使用的,這兩者不一樣。

以 undo_001 表空間為例,表空間編號(hào)為 1?,InnoDB 對(duì) undo_001 表空間進(jìn)行截?cái)嗖僮髦?,?huì)創(chuàng)建一個(gè) 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

崩潰恢復(fù)過程中,InnoDB 如果發(fā)現(xiàn)某個(gè)表空間存在對(duì)應(yīng)的 trunc.log 文件,說明這個(gè) undo 表空間在 MySQL 崩潰時(shí)正在進(jìn)行截?cái)嗖僮鳌?/p>

但是,只通過 trunc.log 文件存在這一個(gè)條件,并不能確定 undo 表空間截?cái)嗖僮鳑]有完成,還要進(jìn)一步判斷。

接著讀取 trunc.log 文件的內(nèi)容,把讀到的內(nèi)容轉(zhuǎn)換成數(shù)字,判斷這個(gè)數(shù)字是不是等于 76845412。

76845412 是什么?稍候介紹。

如果等于,說明在 MySQL 崩潰之前,undo 表空間截?cái)嗖僮饕呀?jīng)完成,只是 trunc.log 文件還沒來得及刪除。此時(shí),直接刪除這個(gè)文件就可以了。

如果不等于,說明 MySQL 崩潰時(shí),undo 表空間截?cái)嗖僮鬟€沒有完成,那就需要繼續(xù)完成。此時(shí),直接刪除 undo 表空間文件。

被刪除的 undo 表空間要等到初始化事務(wù)子系統(tǒng)之后,才會(huì)重建,重建過程我們稍后介紹。

舉個(gè)例子:啟動(dòng)過程中發(fā)現(xiàn)了 undo_001 表空間對(duì)應(yīng)的 trunc.log 文件,并且文件中存儲(chǔ)的數(shù)字不是 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 表空間對(duì)應(yīng)的 trunc.log 文件一起刪除?

因?yàn)?undo 表空間要等到初始化事務(wù)子系統(tǒng)完成之后再重建,而 trunc.log 是 undo 表空間重建的憑證,所以,現(xiàn)在還不能刪除。

接下來我們?cè)倏纯?trunc.log 文件的創(chuàng)建和寫入過程。

InnoDB 進(jìn)行 undo 表空間截?cái)嗖僮髦埃蜁?huì)創(chuàng)建 trunc.log 文件(大小為 innodb_page_size 字節(jié)),并把文件內(nèi)容的所有字節(jié)都初始化為 NULL,然后開始進(jìn)行 undo 表空間截?cái)嗖僮鳌?/p>

操作完成之后,會(huì)往 trunc.log 文件中寫入一個(gè)被稱為魔數(shù)的數(shù)字:76845412,用于標(biāo)識(shí) undo 表空間截?cái)嗖僮饕呀?jīng)完成。

如果魔數(shù)成功寫入 trunc.log 文件,接下來會(huì)把 trunc.log 文件刪除,undo 表空間的截?cái)嗖僮骶徒Y(jié)束了。

5、初始化事務(wù)子系統(tǒng)

現(xiàn)在,我們來到了初始化事務(wù)子系統(tǒng)階段。

InnoDB 之所以把初始化事務(wù)子系統(tǒng)安排在刪除 undo 表空間之后,有可能是為了避免讀取要被刪除的 undo 表空間,能夠節(jié)省一點(diǎn)點(diǎn)時(shí)間。

刪除還沒有完成截?cái)嗖僮鞯?undo 表空間文件之后,剩下的 undo 表空間文件都需要讀取。

從 undo 表空間文件讀取未完成的事務(wù),初始化事務(wù)子系統(tǒng),主要過程如下:

初始化事務(wù)子系統(tǒng)還包含其它操作,不在本文介紹的范圍內(nèi)。

第 1 步,從內(nèi)存中的 undo 表空間對(duì)象數(shù)組中讀取 undo 表空間信息。

undo 表空間默認(rèn)為 2 個(gè),最多可以有 127 個(gè)。

有了獨(dú)立 undo 表空間之后,位于系統(tǒng)表空間中的回滾段就已經(jīng)不再使用了,所以不需要從系統(tǒng)表空間的回滾段中讀取事務(wù)信息。

第 2 步,從 undo 表空間中頁號(hào) = 3 的數(shù)據(jù)頁中讀取回滾段。

每個(gè) undo 表空間可以有 1 ~ 128 個(gè)回滾段,由系統(tǒng)變量 innodb_rollback_segments 控制,默認(rèn)值為 2.

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

回滾段的段頭頁中有 1024 個(gè) undo slot(4 字節(jié)),每個(gè) undo slot 對(duì)應(yīng)一個(gè) undo 段。

如果 undo slot 的值 等于 FIL_NULL,表示這個(gè) undo slot 沒有關(guān)聯(lián)到 undo 段,繼續(xù)執(zhí)行第 3 步,讀取下一個(gè) undo slot。

如果 undo slot 的值 不等于 FIL_NULL,表示這個(gè) undo slot 關(guān)聯(lián)了 undo 段,進(jìn)入第 4 步。

第 4 步,從 undo slot 對(duì)應(yīng)的 undo 段中讀取未完成事務(wù)的信息。

此時(shí),undo slot 的值就是 undo 段的段頭頁的頁號(hào),通過這個(gè)頁號(hào)可以讀取到 undo 段中的事務(wù)信息。

undo slot 關(guān)聯(lián)了 undo 段,說明數(shù)據(jù)庫崩潰時(shí),undo 段中的事務(wù)還沒有完成,事務(wù)狀態(tài)可能是以下 3 種之一:

  • TRX_STATE_ACTIVE,表示事務(wù)還沒有進(jìn)入提交階段。
  • TRX_STATE_PREPARED,表示事務(wù)已經(jīng)提交了,但是只完成了二階段提交的 PREPARE 階段,還沒有完成 COMMIT 階段。
  • TRX_STATE_COMMITTED_IN_MEMORY,表示事務(wù)已經(jīng)完成了二階段提交的 2 個(gè)階段,還剩一些收尾工作沒做,這種狀態(tài)的事務(wù)修改的數(shù)據(jù)已經(jīng)可以被其它事務(wù)看見了。
  • 事務(wù)的收尾工作有哪些?清理已提交事務(wù)小節(jié)會(huì)介紹。

第 1 ~ 4 步是個(gè)循環(huán)的過程,直到讀完所有 undo 表空間中的事務(wù)信息結(jié)束。

6、重建 undo 表空間

對(duì)于存在 trunc.log 文件的 undo 表空間,因?yàn)橹?undo 表空間文件被刪除了,現(xiàn)在要開始著手重建 undo 表空間了,主要流程如下:

第 1 步,創(chuàng)建 trunc.log 文件,標(biāo)記 undo 表空間重建操作正在進(jìn)行中。

看到這里你可能會(huì)奇怪,undo 表空間對(duì)應(yīng)的 trunc.log 文件不是沒有刪除嗎?這里為什么又要?jiǎng)?chuàng)建一次?

別急,且往下看。

在創(chuàng)建 undo 表空間對(duì)應(yīng)的 trunc.log 文件之前,會(huì)先刪除之前舊的 trunc.log 文件,然后創(chuàng)建新的 trunc.log 文件。

新舊 trunc.log 文件名是一樣的,例如:對(duì)于 undo_001 表空間來說,新舊 trunc.log 文件名都是 undo_1_trunc.log。

?為什么要?jiǎng)h除舊的 trunc.log 文件再創(chuàng)建新的同名 trunc.log 文件呢?

因?yàn)橹亟? undo 表空間和新建 undo 表空間是同一套邏輯,而新建 undo 表空間之前,該表空間并不存在對(duì)應(yīng)的 trunc.log 文件。

為了保持統(tǒng)一的邏輯,所以會(huì)先刪除已經(jīng)存在的 trunc.log 文件。

第 2 步,創(chuàng)建 undo 表空間文件,初始大小為 16M,這個(gè)大小是硬編碼的。

第 3 步,初始化 undo 表空間,把表空間 ID、各種鏈表信息寫入表空間的 0 號(hào)頁中,然后分配一個(gè)新的數(shù)據(jù)頁,創(chuàng)建并初始化回滾段,回滾段數(shù)量由系統(tǒng)變量 innodb_rollback_segments 控制。

第 4 步,循環(huán) undo 表空間中的所有回滾段,把每個(gè)回滾段中的 1024 個(gè) undo slot 都初始化為 FIL_NULL。

第 5 步,標(biāo)記 undo 表空間重建操作已經(jīng)完成。

InnoDB 會(huì)先往 trunc.log 文件中寫入一個(gè)魔數(shù) 76845412,表示重建表空間操作已經(jīng)完成。

寫入魔數(shù)成功之后,再把 trunc.log 文件刪除,重建一個(gè) undo 表空間的過程就結(jié)束了。

如果有多個(gè) undo 表空間需要重建,對(duì)于每個(gè) undo 表空間都需要進(jìn)行 1 ~ 5 步的流程。

7、處理事務(wù)

在初始化事務(wù)子系統(tǒng)小節(jié),我們介紹過,從 undo 表空間中讀取出來的事務(wù)有 3 種狀態(tài):

  • TRX_STATE_ACTIVE。
  • TRX_STATE_PREPARED。
  • TRX_STATE_COMMITTED_IN_MEMORY。

處理事務(wù)階段對(duì)這 3 種狀態(tài)會(huì)進(jìn)行不同的處理,請(qǐng)接著往下看。

(1)清理已提交事務(wù)

這里要清理的已提交事務(wù),指的是狀態(tài)為 TRX_STATE_COMMITTED_IN_MEMORY 的事務(wù),包含 DDL 和 DML 事務(wù)。

這種狀態(tài)的事務(wù)已經(jīng)完成二階段提交的 PREPARE 和 COMMIT 階段,是已經(jīng)提交成功的事務(wù),只差最后一點(diǎn)點(diǎn)清理工作,它們修改的數(shù)據(jù)已經(jīng)能被其它事務(wù)看見了。

清理工作主要有幾點(diǎn):

  • 處理 insert undo 段。如果 insert undo 段能被緩存,undo 段會(huì)被加入 insert_undo_cached 鏈表尾部,以備重復(fù)使用;如果 insert undo 段不能被緩存,undo 段就會(huì)被釋放。
  • 把事務(wù)從讀寫事務(wù)鏈表中刪除。
  • 把事務(wù)狀態(tài)修改為TRX_STATE_NOT_STARTED。

(2)回滾未提交 DDL 事務(wù)

未提交事務(wù)指的是狀態(tài)為 TRX_STATE_ACTIVE 的事務(wù),也就是活躍事務(wù)。

崩潰恢復(fù)過程中,這種狀態(tài)的事務(wù)是需要直接回滾的。

你可能會(huì)有個(gè)疑問,DDL 事務(wù)不是不能回滾嗎?

DDL 事務(wù)不能回滾,這只是針對(duì) MySQL 用戶而言,MySQL 內(nèi)部并不會(huì)受到這個(gè)限制。

我們?cè)谑褂?MySQL 的過程中,如果在一個(gè) DML 事務(wù)中間執(zhí)行了一條 DDL 語句,會(huì)觸發(fā)隱式提交,直接把 DML 事務(wù)提交了。

然后 DDL 會(huì)開啟一個(gè)新事務(wù),這個(gè)新事務(wù)是自動(dòng)提交的,DDL 執(zhí)行完成之后,事務(wù)就直接提交了,我們是沒有機(jī)會(huì)對(duì) DDL 事務(wù)進(jìn)行回滾操作的。

MySQL 沒給我們回滾 DDL 事務(wù)的機(jī)會(huì),但是它自己有這個(gè)特權(quán)。

(3)回滾未提交 DML 事務(wù)

未提交的 DDL 事務(wù)和 DML 事務(wù)在源碼中是在不同時(shí)間觸發(fā)的,它的回滾過程和 DDL 事務(wù)一樣。

事務(wù)回滾的過程比較復(fù)雜,本文我們就不展開說了,后續(xù)會(huì)寫一篇文章專門介紹事務(wù)回滾的過程。

(4)處理 PREPARE 事務(wù)

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

PREPARE 事務(wù)提交還是回滾,取決于這個(gè)事務(wù)的 XID 是否已經(jīng)寫入到 binlog 日志文件中。

事務(wù) XID 是以 binlog event 的方式寫入 binlog 日志文件的,event 的名字是 XID_EVENT。

一個(gè)事務(wù)只會(huì)有一個(gè) XID,也就只會(huì)有一個(gè) XID_EVENT 了。

要知道事務(wù)的 XID_EVENT 是否已經(jīng)寫入到 binlog 日志文件,需要先讀取 binlog 日志文件。

從上面的介紹可以看到,處理 PREPARE 事務(wù)依賴于 binlog 日志文件,因此,這部分邏輯是在打開 binlog 日志文件的過程中實(shí)現(xiàn)的。

MySQL 在同一時(shí)刻只會(huì)往一個(gè) binlog 日志文件中寫入  binlog event,在崩潰那一刻,承載寫入 event 的文件是最后一個(gè) binlog 日志文件。

因此,崩潰恢復(fù)過程中,只需要掃描最后一個(gè) binlog 日志文件,找到其中所有的 XID_EVENT, 用于判斷 PREPARE 事務(wù)的 XID_EVENT 是否已經(jīng)寫入 binlog 日志文件。

如果 MySQL 上一次是正常關(guān)閉,啟動(dòng)過程中,不會(huì)存在沒有完成的事務(wù),沒有 PREPARE 事務(wù)需要處理,也就不用掃描最后一個(gè) binlog 日志文件了。

MySQL 怎么知道上一次是不是正常關(guān)閉呢?

每個(gè) binlog 日志文件的第 1 個(gè) EVENT 都是 FORMAT_DESCRIPTION_EVENT,用于描述 binlog 日志文件格式信息,這個(gè) EVENT 中包含一個(gè)標(biāo)記 LOG_EVENT_BINLOG_IN_USE_F。

binlog 日志文件創(chuàng)建時(shí),這個(gè)標(biāo)記位會(huì)被設(shè)置為 1,表示 binlog 日志文件正在被使用。

LOG_EVENT_BINLOG_IN_USE_F 標(biāo)記在 2 種情況下會(huì)被清除:

  • 切換 binlog 日志文件時(shí),舊 binlog 日志文件的LOG_EVENT_BINLOG_IN_USE_F 標(biāo)記會(huì)被清除。
  • MySQL 正常關(guān)閉時(shí),正在使用的 binlog 日志文件的LOG_EVENT_BINLOG_IN_USE_F 標(biāo)記會(huì)被清除。

如果 MySQL 突然崩潰,來不及把這個(gè)標(biāo)記設(shè)置為 0。

那么下次啟動(dòng)時(shí),MySQL 讀取最后一個(gè) binlog 日志文件的 FORMAT_DESCRIPTION_EVENT 發(fā)現(xiàn) LOG_EVENT_BINLOG_IN_USE_F 標(biāo)記為 1,就會(huì)進(jìn)入處理 PREPARE 事務(wù)階段,主要流程如下:

第 1 步,掃描最后一個(gè) binlog 日志文件,讀取 EVENT,找到其中所有的 XID_EVENT,并把讀取到的事務(wù) XID 存放到一個(gè)集合中。

第 2 步,InnoDB 循環(huán)讀寫事務(wù)鏈表,每找到一個(gè) PREPARE 事務(wù)都存放到數(shù)組中,最后把數(shù)組返回給 server 層。

第 3 步,讀取 InnoDB 返回的 PREPARE 事務(wù)數(shù)組,判斷事務(wù) XID 是否在第 1 步的事務(wù) XID 集合中。

第 4 步,提交或回滾事務(wù)。

如果事務(wù) XID 在集合中,說明 MySQL 崩潰之前,事務(wù) XID_EVENT 就已經(jīng)寫入 binlog 日志文件了。

XID_EVENT 有可能已經(jīng)同步給從服務(wù)器,從服務(wù)器上可能已經(jīng)重放了這個(gè)事務(wù)。

這種情況下,為了保證主從數(shù)據(jù)的一致性,事務(wù)在主服務(wù)器上也需要提交。

如果事務(wù) XID 不在集合中,說明 MySQL 崩潰之前,事務(wù) XID_EVENT 沒有寫入 binlog 日志文件。

XID_EVENT 肯定也就沒有同步給從服務(wù)器了,同樣為了保證主從數(shù)據(jù)的一致性,事務(wù)在主服務(wù)器上也不能提交,而是需要回滾。

3 ~ 4 步是個(gè)循環(huán)過程,循環(huán)完 InnoDB 返回的 PREPARE 事務(wù)數(shù)組之后,處理 PREPARE 事務(wù)的過程結(jié)束,崩潰恢復(fù)主要流程也就完成了。

8、總結(jié)

MySQL 崩潰恢復(fù)過程的核心工作有 2 點(diǎn):

  • 對(duì)于 MySQL 崩潰之前還沒有刷新到磁盤的數(shù)據(jù)頁(也就是臟頁),用 Redo 日志把這些數(shù)據(jù)頁恢復(fù)到 MySQL 崩潰之前那一刻的狀態(tài),這相當(dāng)于對(duì)臟頁進(jìn)行一次刷盤操作。在這之前,需要用兩次寫緩沖區(qū)中的頁把損壞的數(shù)據(jù)頁修復(fù)為正常狀態(tài),然后才能在此基礎(chǔ)上用 Redo 日志恢復(fù)數(shù)據(jù)頁。
  • 清理、提交、回滾還沒有完成的事務(wù)。

    對(duì)于已完成二階段提交的 PREPARE、COMMIT 2 個(gè)階段的事務(wù),做收尾工作。

    對(duì)于活躍狀態(tài)的事務(wù),直接回滾。

    對(duì)于 PREPARE 狀態(tài)的事務(wù),如果事務(wù) XID 已寫入 binlog 日志文件,提交事務(wù),否則回滾事務(wù)。

本文轉(zhuǎn)載自微信公眾號(hào)「一樹一溪」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹一溪公眾號(hào)。


本文標(biāo)題:MySQL崩潰恢復(fù)過程分析
本文地址:http://www.5511xx.com/article/djiihdh.html