Undo 日志用什么存儲結(jié)構(gòu)支持無鎖并發(fā)寫入?
redo 日志只有崩潰恢復(fù)的時候才能派上用場,undo 日志不一樣,它承擔(dān)著多重職責(zé),MySQL 崩潰恢復(fù)、以及正常提供服務(wù)期間,都有它的身影。
按照使用頻次,undo 日志的多重職責(zé)如下:
職責(zé) 1,為 MVCC 服務(wù),減少讀寫事務(wù)之間的相互影響,提升數(shù)據(jù)庫的并發(fā)能力。
職責(zé) 2,保證數(shù)據(jù)庫運行過程中的數(shù)據(jù)一致性。事務(wù)回滾時,把事務(wù)中被修改的數(shù)據(jù)恢復(fù)到修改之前的狀態(tài)。
職責(zé) 3,保證數(shù)據(jù)庫崩潰之后的數(shù)據(jù)一致性。崩潰恢復(fù)過程中,恢復(fù)沒有完成提交的事務(wù),并根據(jù)事務(wù)的狀態(tài)和 binlog 日志是否寫入了該事務(wù)的 xid 信息,共同決定事務(wù)是提交還是回滾。
undo 日志需要為數(shù)據(jù)一致性和 MVCC 服務(wù),除了要支持多事務(wù)同時寫入日志,還要支持多事務(wù)同時讀取日志。
為了有更好的讀寫并發(fā)性能,它擁有與 redo 日志完全不一樣的存儲結(jié)構(gòu)。
本文我們就來聊聊 undo 日志的存儲結(jié)構(gòu),它是怎么寫入 undo 日志文件的,以及事務(wù)二階段提交過程中和它有關(guān)的操作。
本文內(nèi)容基于 MySQL 8.0.29 源碼。
正文
1、 概述
undo 日志的存儲結(jié)構(gòu)比較復(fù)雜,我們先以倒序的方式來介紹一下存儲結(jié)構(gòu)的各個部分,以便大家有個整體了解。
undo log header:一個事務(wù)可能產(chǎn)生多條 undo 日志,也可能只產(chǎn)生一條 undo 日志,不管事務(wù)產(chǎn)生了多少條 undo 日志,這些日志都?xì)w屬于事務(wù)對應(yīng)的日志組,日志組由 undo log header 負(fù)責(zé)管理。
undo 頁:undo log header 和 undo 日志都存儲于 undo 頁中。
undo 段:為了多個事務(wù)同時寫 undo 日志不相互影響,undo 日志也使用了無鎖設(shè)計,InnoDB 會為每個事務(wù)分配專屬的 undo 段,每個事務(wù)只會往自己專屬 undo 段的 undo 頁中寫入日志。
一個 undo 段可能會包含一個或多個 undo 頁,多個 undo 頁會形成 undo 頁面鏈表。
inode:每個 undo 段都會關(guān)聯(lián)一個 inode。
undo 段本身并不具備管理 undo 頁的能力,有了 inode 這個外掛之后,undo 段就可以管理段中的 undo 頁了。
回滾段:一個事務(wù)可能會需要 1 ~ 4 個 undo 段,很多個事務(wù)同時執(zhí)行,就需要很多個 undo 段,這些 undo 段需要有一個地方統(tǒng)籌管理,這個地方就是回滾段。
undo slot:一個回滾段管理著 1024 個 undo 段,每個回滾段的段頭頁中都有 1024 個小格子,用來記錄 undo 段中第一個 undo 頁的頁號,這個小格子就叫作 undo slot。
重要說明: 一個回滾段管理 1024 個 undo 段,這是基于 innodb_page_size 是 16K(默認(rèn)值) 的情況,本文涉及到頁大小的內(nèi)容,都以 16K 的頁為前提,后面就不再單獨說明了。
undo 表空間:一個回滾段能夠管理 1024 個 undo 段,看起來已經(jīng)很多了,假設(shè)每個事務(wù)都只需要 1 個 undo 段,如果只有一個回滾段也只能支持 1024 個事務(wù)同時執(zhí)行。
對于擁有幾十核甚至百核以上 CPU 的服務(wù)器來說,這顯然會限制它們的發(fā)揮。
為了充分發(fā)揮服務(wù)器的能力,有必要支持更多事務(wù)的同時執(zhí)行,所以就有了 undo 表空間,一個 undo 表空間最多可以支持 128 個回滾段。
不止于此,InnoDB 還能夠最多支持 127 個 undo 表空間,這樣算起來,所有回滾段總共能夠管理的 undo 段數(shù)量是:1024 * 128 * 127 = 16646144。
這么多 undo 段,是不是瞬間就有了地主家有余糧的感覺?
看了上面的介紹,相信大家能對 undo 日志有個整體了解,接下來我們就按照這個整體結(jié)構(gòu),自頂向下來詳細(xì)介紹其中的每個部分。
2、undo 表空間
一個獨立的 undo 表空間對應(yīng)磁盤上的一個文件。
MySQL 8.0 開始,強(qiáng)制開啟了獨立的 undo 表空間,支持創(chuàng)建 2 ~ 127 個 undo 表空間,默認(rèn)數(shù)量為 2,可以通過 CREATE UNDO TABLESPACE 增加 undo 表空間,通過 DROP UNDO TABLESPACE 減少 undo 表空間。
每個 undo 表空間都可以配置 1 ~ 128 個回滾段,可以通過系統(tǒng)變量 innodb_rollback_segments 來控制每個 undo 表空間中的回滾段數(shù)量,默認(rèn)值為 128。
每個 undo 表空間中,page_no = 3 的頁專門用于保存回滾段的段頭頁的頁號,這個頁的類型是 FIL_PAGE_TYPE_RSEG_ARRAY,從 Offset 56 開始,保存著 128 個回滾段的段頭頁的頁號,如下圖所示:
3、回滾段
(1)什么是回滾段?
InnoDB 中凡是被稱為段的東西,都是用來管理數(shù)據(jù)頁的一種邏輯結(jié)構(gòu)。
回滾段也不例外,它也是管理數(shù)據(jù)頁的一種邏輯結(jié)構(gòu)。
回滾段管理了什么頁呢?
回滾段有一點點特殊,它只管理一個頁,就是回滾段的段頭頁。
每個回滾段中只有段頭頁這一個數(shù)據(jù)頁,由此可見,管理數(shù)據(jù)頁并不是它最重要的職責(zé)。
在概述小節(jié),我們介紹過,每個回滾段中都有 1024 個 undo slot,可以統(tǒng)籌管理 1024 個 undo 段,這才是回滾段最重要的職責(zé)。
基于前面的介紹,我們可以給回滾段下一個定義了:回滾段是一種邏輯結(jié)構(gòu),它負(fù)責(zé)段頭頁的分配,以及管理其中 1024 個 undo slot 對應(yīng)的 undo 段。
(2)分配回滾段
開啟一個讀寫事務(wù),或者一個只讀事務(wù)轉(zhuǎn)換為讀寫事務(wù)時,InnoDB 會為事務(wù)分配一個回滾段。
默認(rèn)配置下,2 個 undo 表空間總共有 256 個回滾段,這么多回滾段,就涉及到怎么均衡使用的問題了。
2 個 undo 表空間,在內(nèi)存是中一個數(shù)組,下標(biāo)分別為 0、1。
每個 undo 表空間中 128 個回滾段,在內(nèi)存中也是一個數(shù)組,下標(biāo)為 0 ~ 127。
以 undo 表空間下標(biāo)、回滾段下標(biāo)組成一個元組,用于表示默認(rèn)配置下的 256 個回滾段,如下:
(0, 0)、(0, 1)、…、(0, 126)、 (0, 127)
(1, 0)、(1, 1)、…、(1, 126)、 (1, 127)
分配回滾段的邏輯是按照 undo 表空間、回滾段輪流著來,順序是這樣的:
(0, 0)、(1, 0)、(0, 1)、(1, 1)、……、(0, 126)、(1, 126)、(0, 127)、(1, 127)
分配順序用圖片展示是這樣的(按照箭頭順序分配):
每次分配時,都會記錄這次分配的是哪個回滾段。
下次再分配時,按照上面的順序,把最后一次分配的回滾段之后的那個回滾段分配給事務(wù)。
InnoDB 中的回滾段,分為普通表回滾段、用戶臨時表回滾段,前面介紹的是普通表回滾段分配邏輯。
用戶臨時表只有一個獨立 undo 表空間,默認(rèn) 128 個回滾段,需要分配臨時表回滾段時,只要輪流分配就行了。
4、undo slot
(1)什么是 undo slot?
每個回滾段的段頭頁中都有 1024 個小格子,每個小格子就是一個 undo slot,用于記錄分配給事務(wù)的 undo 段的段頭頁頁號,如下圖所示:
(2)尋找 undo slot
一條 DML 語句即將要修改數(shù)據(jù)之前,會先記錄 undo 日志。
記錄 undo 日志之前,需要先創(chuàng)建一個 undo 段。
undo 段要把自己交給回滾段管理,這需要在回滾段的段頭頁中找一個 undo slot 占個位。
尋找 undo slot 的過程簡單粗暴,從回滾段 1024 個 slot 中的第一個 slot 開始遍歷,讀取 slot 的值,只要 slot 的值等于 FIL_NULL,就說明這個 slot 沒有被別的 undo 段占用,當(dāng)前 undo 就可以占上這個位置。
FIL_NULL 是 32 位無符號整數(shù)的最大值,十六進(jìn)制表示為 0xFFFFFFFFUL,十進(jìn)制表示為 4294967295。
如果遍歷到最后一個 slot,都沒有發(fā)現(xiàn) 值 = FIL_NULL 的 slot,那就說明分配給當(dāng)前事務(wù)的回滾段沒有可用的 slot 了。
這種情況下,InnoDB 并不會再重新給事務(wù)分配一個回滾段,而是直接報錯:Too many active concurrent transactions。
5、undo 段
(1)什么是 undo 段?
undo 段,也是段字輩,那它自然也是管理數(shù)據(jù)頁的一種邏輯結(jié)構(gòu)了。
undo 段管理的數(shù)據(jù)頁就是用來存放 undo 日志的頁,也就是 undo 頁。
按照對于表的增、刪、改操作是否需要記錄 redo 日志來分類,undo 段可以分為 2 種類型:
臨時表 undo 段,對于用戶臨時表的增、刪、改操作,數(shù)據(jù)庫崩潰之后重新啟動,不需要恢復(fù)這些表里的數(shù)據(jù),也就是說臨時表里的數(shù)據(jù)不需要保證持久性,因此不需要記錄 redo 日志。
但是,如果事務(wù)對用戶臨時表進(jìn)行了增、刪、改操作,事務(wù)回滾時,用戶臨時表中的數(shù)據(jù)也需要回滾,所以需要記錄 undo 日志。
普通表 undo 段,對于普通表的增、刪、改操作,數(shù)據(jù)庫崩潰之后重新啟動,需要把這些操作修改過的數(shù)據(jù),恢復(fù)到數(shù)據(jù)庫崩潰時的狀態(tài),所以需要記錄 redo 日志。
事務(wù)回滾時,對于普通表進(jìn)行的增、刪、改操作,表中的數(shù)據(jù)也需要回滾,所以需要記錄 undo 日志。
按照增、刪、改操作來分類,undo 段也可以分為 2 種類型:
- insert undo 段,用于保存 insert 語句產(chǎn)生的 undo 日志的 undo 段。
- update undo 段,用于保存 update、delete 語句產(chǎn)生的 undo 日志的 undo 段。
為什么要區(qū)分 insert undo 段和 update undo 段?
因為 insert 語句產(chǎn)生的 undo 日志,在事務(wù)提交時,如果 undo 段不能被緩存起來復(fù)用,就會直接釋放。
update、delete 語句產(chǎn)生的 undo 日志,在事務(wù)提交時,如果 undo 段不能被緩存起來復(fù)用,也不會直接釋放,而是要服務(wù)于 MVCC。
等到 undo 日志中的歷史版本數(shù)據(jù)不再被其它事務(wù)需要時,這些 undo 日志才能被清除。
關(guān)于 undo 日志什么時候能被清除的細(xì)節(jié),留到 purge 線程清理 undo 日志的文章再寫。
此時,如果 undo 日志所在的 undo 段中沒有其它有效的 undo 日志時,undo 段才能被釋放。
按照前面的 2 種維度分類,可以形成 4 種類型的 undo 段:
- 插入記錄到用戶臨時表,是臨時表 insert undo 段。
- 更新、刪除用戶臨時表中的記錄,是臨時表 update undo 段。
- 插入記錄到普通表,是普通表 insert undo 段。
- 更新、刪除普通表中的記錄,是普通表 update undo 段。
在同一個事務(wù)中,以上 4 種類型的 undo 段都有可能出現(xiàn),所以,一個事務(wù)中就可能會需要 1 ~ 4 個 undo 段。
(2)復(fù)用緩存的 undo 段
每創(chuàng)建一個 undo 段,需要經(jīng)過一系列的操作:
- 從 inode 頁中找到一個未被使用的 inode。
- 分配一個 inode 頁(可能需要)。
- 為 undo 段分配一個 undo 頁。
- 初始化內(nèi)存中的 undo 段對象。
- 初始化內(nèi)存中的 undo log header 對象。
- 其它操作 ...
這些初始化操作都是需要時間的,頻繁創(chuàng)建就有點浪費時間了。為此,InnoDB 設(shè)定了一個規(guī)則,在事務(wù)提交時,符合規(guī)則的 undo 段就可以被緩存起來,給后面的事務(wù)重復(fù)使用。
undo 段可緩存復(fù)用的規(guī)則,本文后面二階段提交的 commit 階段會介紹。
前面介紹過,事務(wù)中使用 undo 段時,按照 2 種維度分類會形成 4 種類型的 undo 段,這是不是有點復(fù)雜?
undo 段緩存就比較簡單了,只分了 2 種:insert undo 段、update undo 段。
有了 undo 段緩存之后,就不需要每次分配 undo 段時都從頭開始創(chuàng)建一個了。
如果要為用戶臨時表、普通表的 insert 語句分配一個 undo 段,就去 insert_undo_cached 鏈表中(緩存 insert undo 段的鏈表)看看有沒有 undo 段可以復(fù)用。
如果有,就取鏈表中的第一個 undo 段來用;如果沒有,就創(chuàng)建一個新的 insert undo 段。
如果要為用戶表、普通表的 update、delete 語句分配一個 undo 段,就去 update_undo_cached 鏈表中(緩存 update undo 段的鏈表)看看有沒有 undo 段可以復(fù)用。
如果有,就取鏈表中的第一個 undo 段來用;如果沒有,就創(chuàng)建一個新的 update undo 段。
(3)創(chuàng)建 undo 段
InnoDB 給事務(wù)分配一個 undo 段時,如果沒有緩存的 undo 段可以復(fù)用,需要創(chuàng)建一個新的 undo 段。
創(chuàng)建一個新的 undo 段,會經(jīng)歷以下幾個主要步驟:
第 1 步,找到一個 inode,undo 段會關(guān)聯(lián)一個 inode,用于管理段中的頁。
inode 后面會有一個小節(jié)單獨介紹,這里先不展開。
第 2 步,從表空間 0 號頁的 File Space Header 中讀取 FSP_SEG_ID,作為新創(chuàng)建的 undo 段的 ID(seg_id),把 seg_id 寫入 inode 的 FSEG_ID 字段,表示這個 inode 已經(jīng)被占用了。
第 3 步,通過 inode 分配一個新的空閑頁作為 undo 段的段頭頁。
每個 undo 段都會有一個 Undo Segment Header,位于 undo 段的段頭頁中,如下圖所示:
第 4 步,把 inode 的地址信息寫入 Undo Segment Header 的 TRX_UNDO_FSEG_HEADER 字段。
inode 的地址由 3 個部分組成:
- inode 所在頁的表空間 ID。
- inode 所在頁的頁號。
- inode 在頁中的 Offset。
第 5 步,把段頭頁加入 undo 頁面鏈表的最后,undo 頁面鏈表的基結(jié)點位于 Undo Segement Header 的 TRX_UNDO_PAGE_LIST 字段中。
第 6 步,把 undo 段的段頭頁頁號寫入回滾段中分配給當(dāng)前 undo 段的 undo slot 中,表示這個 undo slot 被占用了。
經(jīng)過以上步驟后,undo 段就創(chuàng)建成功了,可以繼續(xù)進(jìn)行接下來的操作了。
6、undo log header
(1)什么是 undo log header?
一個事務(wù)產(chǎn)生的 undo 日志屬于一個日志組,undo log header 是日志組的頭信息,各字段如下圖所示:
介紹幾個主要字段:
- TRX_UNDO_TRX_ID,產(chǎn)生這組 undo 日志的事務(wù) ID。
- TRX_UNDO_TRX_NO,事務(wù)的提交號,事務(wù)提交時會修改這個字段的值。
- TRX_UNDO_NEXT_LOG,undo 段中下一組 undo 日志的 undo log header 在頁中的 Offset。
- TRX_UNDO_PREV_LOG,undo 段中上一組 undo 日志的 undo log header 在頁中的 Offset。
- TRX_UNDO_HISTORY_NODE?,表示這組 undo 日志是 history 鏈表的一個結(jié)點,purge 線程清理 TRX_UNDO_UPDATE 類型的 undo 日志時會用到這個字段。
(2)復(fù)用 undo log header
如果分配給事務(wù)的 insert undo 段,是從 insert_undo_cached 鏈表中獲取的,undo 段中的 undo log header 是可以直接復(fù)用的,但是其中 4 個字段需要重新初始化:
- TRX_UNDO_TRX_ID,寫入新的事務(wù) ID。
- TRX_UNDO_LOG_START,重置為 undo log header 之后的位置,表示可以寫 undo 日志的位置。
- TRX_UNDO_FLAGS,undo 日志組的標(biāo)記重置為 0。
- TRX_UNDO_DICT_TRANS,表示當(dāng)前這組 undo 日志是否由 DDL 語句事務(wù)產(chǎn)生。
由于 update undo 段中的 undo 日志未被清理之前都需要為 MVCC 服務(wù),如果分配給事務(wù)的 update undo 段是復(fù)用的 undo 段,不能復(fù)用其中的 undo log header,而是會生成一個新的 undo log header,追加到上一個事務(wù)生成的 undo 日志之后的位置。
(3)創(chuàng)建 undo log header
新創(chuàng)建一個 insert / update undo 段,或者復(fù)用一個 update undo 段時,都需要創(chuàng)建一個 undo log header。
創(chuàng)建一個新的 undo log header,就是把 undo log header 中的每個字段值按順序?qū)懭?undo 頁中,然后在內(nèi)存中也會生成一個對應(yīng)的結(jié)構(gòu)(struct trx_undo_t),并初始化其中的各個字段。
需要單獨拿出來說的字段有兩個:
- TRX_UNDO_PREV_LOG?,指向 update undo 段中上一個事務(wù)生成的 undo 日志在 update undo 段的段頭頁中的 Offset。
- TRX_UNDO_NEXT_LOG?,指向 update undo 段下一個事務(wù)生成的 undo 日志在 update undo 段的段頭頁中的 Offset。
通過這兩個字段,update undo 段中的多組 undo 日志就形成了鏈表,purge 線程清理 undo 日志時就可以通過鏈表找到 undo 頁中的所有 undo 日志了。
7、inode
(1)什么是 inode?
不管是回滾段、undo 段,還是索引段,只要是段,都會關(guān)聯(lián)一個 inode。
inode 是真正用于管理與它關(guān)聯(lián)的段中數(shù)據(jù)頁的邏輯結(jié)構(gòu),undo 段之所以能夠管理其中的 undo 頁,關(guān)鍵就是因為 undo 段關(guān)聯(lián)了 inode。
inode 結(jié)構(gòu)如下圖所示:
由上圖可見,inode 中有 32 個 fragment page slot,可以管理 32 個碎片頁。
還有 3 個以 extent 為單位管理數(shù)據(jù)頁的鏈表:
頁大小為 16K 時,1 個 extent 中有 64 個頁。
- FSEG_FREE,這個鏈表的每個 extent 中,所有頁都沒有被使用,全都是空閑頁。
- FSEG_NOT_FULL,這個鏈表的每個 extent 中,都有一部分頁已被使用,另一部分頁是空閑頁。
- FSEG_FULL,這個鏈表的每個 extent 中,所有頁都已經(jīng)被使用,沒有空閑頁。
根據(jù)頁的分配規(guī)則,inode 關(guān)聯(lián)的段每分配一個頁,既有可能從 32 個 fragment page slot 中找一個空閑頁,也有可能從 FSEG_FREE、FSEG_NOT_FULL 中找一個空閑頁。
(2)分配 inode
undo 表空間中,有專門的 inode 頁用于存放 inode,每個 inode 占用 192 字節(jié),16K 的 inode 頁最多能夠存放 85 個 inode,如下圖所示:
undo 表空間 0 號頁的 File Space Header 中,有 2 個管理 inode 頁的鏈表:
- FSP_SEG_INODES_FULL,這個鏈表中的所有 inode 頁都存放了 85 個 inode,不能再存入新的 inode。
- FSP_SEG_INODES_FREE,這個鏈表中的所有 inode 頁都還有空閑空間可以存入新的 inode。
File Space Header 中的各字段如下圖所示:
每次為事務(wù)創(chuàng)建一個新的 undo 段之前,都會先從 FSP_SEG_INODES_FREE 鏈表的第一個 inode 頁中獲取一個可用的 inode。
從 inode 頁中獲取 inode 的邏輯簡單粗暴:
從 inode 頁中的第一個 inode 開始遍歷,直到找到一個 FSP_SPACE_ID 字段值為 0 的 inode,表示這個 inode 未被其它 undo 段占用,可以分配給當(dāng)前 undo 段。
不過,有可能會出現(xiàn)一個意外情況,就是 FSP_SEG_INODES_FREE 鏈表中沒有可用的 inode 頁。
這種情況下,需要先從 undo 表空間中分配一個碎片頁,用作 inode 頁,然后再按照前面介紹的分配 inode 邏輯,給當(dāng)前 undo 段分配一個 inode。
8、寫 undo 日志
本文不會詳細(xì)介紹 undo 日志的格式,但是,每一種類型的 undo 日志中,都有 2 個字段,用于把 undo 日志組中的多條日志組成日志鏈表,需要介紹一下。
每一條 undo 日志中,第一個字段是 next_record_start,占用 2 字節(jié),保存著下一條 undo 日志的第一個字節(jié)在 undo 頁中的 Offset。
每一條 undo 日志中,最后一個字段是 record_start,占用 2 字節(jié),保存著當(dāng)前這條 undo 日志第一個字節(jié)在 undo 頁中 Offset。
next_record_start、record_start 是為了描述方便而取的名字。
通過這 2 個字段,同一個 undo 頁中的多條 undo 日志可以形成一個雙向鏈表,如下圖所示:
從前往后遍歷 undo 日志時,通過 next_record_start 就可以直接讀取到下一條 undo 日志的 Offset 了。
從后往前遍歷 undo 日志時,通過 record_start 只能讀取到本條日志的 Offset,再讀取本條日志的 Offset - 2 處的字段內(nèi)容,就能得到上一條 undo 日志的 Offset 了。
為什么要把 next_record_start 作為 undo 日志的第一個字段,record_start 作為 undo 日志的最后一個字段?
我第一次看到 undo 日志的這個結(jié)構(gòu),是在看《MySQL 是怎樣運行的》這本書的時候,當(dāng)時感覺這樣的結(jié)構(gòu)很不好理解。
研究完源碼寫本文的時候,我試圖為這個結(jié)構(gòu)找到一個合理的解釋,以方便大家理解。
但是,想了幾種不使用這個結(jié)構(gòu)可能會帶來的壞處(例如:占用更多存儲空間,遍歷 undo 日志的時候需要更多時間等等),都沒有找到必須要這樣設(shè)計的合理解釋。
所以,大家也不用糾結(jié)為什么會這樣設(shè)計,就當(dāng)它是個普通的鏈表指針就好了。
當(dāng)然了,如果有哪位小伙伴發(fā)現(xiàn)了這么設(shè)計的好處,也歡迎在文末留言或者微信交流。
正常寫入 undo 日志的過程比較簡單:
- 先寫 undo 日志數(shù)據(jù)。
- 再寫 next_record_start、record_start 在其所處 undo 頁中的 Offset。
undo 日志是每產(chǎn)生一條就往 undo 日志文件中寫入一條(只是寫到 Buffer Pool 中 undo 頁,由刷盤操作統(tǒng)一刷新到磁盤)。
寫 undo 日志中每個字段的細(xì)節(jié)就不再展開了。
寫 undo 日志的過程中可能會面臨一個臨界點:
前面我們提到過,undo 段是可以復(fù)用的,對于復(fù)用的 insert undo 段,邏輯比較簡單,直接覆蓋 undo 段中原來的日志數(shù)據(jù)就可以了。
對于復(fù)用的 update undo 段,由于其中的 undo 日志還需要為 MVCC 服務(wù),不能被覆蓋,需要把新的 undo 日志追加到原來的 undo 日志之后。
這樣一來就可能會出現(xiàn) 2 種情況:
情況 1,undo 頁中剩余空間足夠?qū)懭胍粭l新的 undo 日志,這種情況就簡單了,直接把新的 undo 日志寫入 undo 頁中剩余的空間。
情況 2,undo 頁中剩余空間不夠?qū)懭胍粭l新的 undo 日志了,這種情況稍微復(fù)雜點,會分三步進(jìn)行:
- 把 undo 頁中剩余空間的所有字節(jié)全部填充為0xff。
- 創(chuàng)建一個新的 undo 頁。
- 把 undo 日志寫入到新的 undo 頁中。
還有一種不應(yīng)該出現(xiàn)的情況:
由于一條 undo 日志內(nèi)容太長,一個空閑的 undo 頁都存不下一條 undo 日志。
正常情況下不會發(fā)生這樣的事情,只有 MySQL 源碼有 bug 的時才會出現(xiàn)。
text、blob 系列大字段,存儲的內(nèi)容長度可能超過 undo 頁的大小,更新操作的 undo 日志有可能會超過 undo 日志的大小嗎?
如果源碼沒 bug 的話,不會超過的,對于 text、blob 系列大字段,記錄 undo 日志時并不是直接把字段內(nèi)容原封不動的寫到 undo 日志里,而是會做一些處理,只會有少量內(nèi)容寫到 undo 日志里。
關(guān)于 text、blob 系列大字段具體會往 undo 日志里寫入什么,如果有小伙伴感興趣,可以留言或者微信交流,后續(xù)我可以再進(jìn)一步研究這些細(xì)節(jié),然后寫篇文章單獨介紹。
9、階段提交之 undo 日志
(1)prepare 階段
在 prepare 階段,undo 日志為事務(wù)做的最重要的 2 件事:
- 修改 undo 段狀態(tài),把 Undo Segement Header 的 TRX_UNDO_STATE? 字段值從 TRX_UNDO_ACTIVE 修改為 TRX_UNDO_PREPARED?,表示 undo 日志對應(yīng)的事務(wù)已經(jīng)進(jìn)入 prepare 階段。
- 把事務(wù) xid 信息寫入 Undo log Header 中。
undo 段狀態(tài)用于崩潰恢復(fù)過程中,標(biāo)記哪些事務(wù)需要恢復(fù),哪些事務(wù)不用恢復(fù)。
xid 信息用于崩潰恢復(fù)過程中,決定數(shù)據(jù)庫崩潰時處于 prepared 階段的事務(wù),是要回滾還是要提交。
(2)commit 階段
到了 commit 階段,insert undo 日志的使命就已經(jīng)結(jié)束了,update undo 日志還需要為 MVCC 服務(wù)。
不管是 insert undo 段還是 update undo 段,只要滿足以下 2 個條件都可以被緩存起來復(fù)用:
- undo 段中只有一個 undo 頁。
- 包括 File Header、Page Header 在內(nèi),undo 頁已使用空間必須小于 undo 頁總字節(jié)數(shù)的四分之三。
對于 insert undo 段,如果能復(fù)用,會進(jìn)行以下操作:
步驟 1,Undo Segment Header 的 TRX_UNDO_STATE 字段值由 TRX_UNDO_PREPARED 變?yōu)?nbsp;TRX_UNDO_CACHED。
步驟 2,事務(wù)對應(yīng) undo 日志組的 undo log header 對象加入回滾段 insert_undo_cached 鏈表的最前面,以備下一個事務(wù)復(fù)用。
如果不能復(fù)用,會進(jìn)行以下操作:
步驟 1,Undo Segement Header 的 TRX_UNDO_STATE 字段值由 TRX_UNDO_PREPARED 變?yōu)?nbsp;TRX_UNDO_TO_FREE。
步驟 2,undo 段關(guān)聯(lián)的 inode 的 FSEG_ID 字段改為 0,表示 inode 可以被其它 undo 段使用,然后釋放 undo 段中分配的所有 undo 頁。
步驟 3,把 insert undo 段占用的 undo slot 值會改為 FIL_NULL,表示這個 undo slot 處于空閑狀態(tài),可以被其它事務(wù)使用了。
對于 update undo 段,如果能復(fù)用,會進(jìn)行以下操作:
步驟 1,Undo Segment Header 的 TRX_UNDO_STATE 字段值由 TRX_UNDO_PREPARED 變?yōu)?nbsp;TRX_UNDO_CACHED。
步驟 2,通過 undo log header 字段 TRX_UNDO_HISTORY_NODE 把 undo 日志組加入 history list 鏈表。
purge 線程通過遍歷 history list 鏈表來清除 undo 日志。
步驟 3,把事務(wù)提交號寫入 undo log header 字段 TRX_UNDO_TRX_NO。
purge 線程用這個字段來判斷 undo 日志是否能夠被清除、標(biāo)記刪除的記錄是否能夠徹底刪除。
步驟 4,事務(wù)對應(yīng) undo 日志組的 undo log header 對象加入回滾段 update_undo_cached 鏈表的最前面,以備下一個事務(wù)復(fù)用。
如果不能復(fù)用,會進(jìn)行以下操作:
步驟 1,Undo Segement Header 的 TRX_UNDO_STATE 字段值由 TRX_UNDO_PREPARED 變?yōu)?nbsp;TRX_UNDO_TO_PURGE。
步驟 2,update undo 段占用的 undo slot 的值改為 FIL_NULL,表示這個 undo slot 處于空閑狀態(tài),可以被其它事務(wù)使用了。
步驟 3,從回滾段 Rollback Segnemt Header 中讀取 TRX_RSEG_HISTORY_SIZE,加上 undo 段中 undo 頁的數(shù)量,然后回寫到 TRX_RSEG_HISTORY_SIZE 中,作為 history list 鏈表中最新的 undo 頁數(shù)量。
undo 能夠復(fù)用時,不會修改 TRX_RSEG_HISTORY_SIZE 字段值。
步驟 4,通過 undo log header 字段 TRX_UNDO_HISTORY_NODE 把 undo 日志組加入 history list 鏈表。
purge 線程通過遍歷 history list 鏈表來清除 undo 日志。
步驟 5,把事務(wù)提交號寫入 undo log header 字段 TRX_UNDO_TRX_NO。
purge 線程用這個字段來判斷 undo 日志是否能夠被清除、標(biāo)記刪除的記錄是否能夠徹底刪除。
小結(jié)一下,commit 階段,就是 undo 段能復(fù)用就復(fù)用,不能復(fù)用就直接清理釋放(insert undo 段),或者等待 purge 線程清理釋放(update undo 段)。
10、總結(jié)
InnoDB 支持 2 ~ 127 個獨立表空間,每個表空間支持 1 ~ 128 個回滾段,每個回滾段支持 1024 個 undo slot,可以管理 1024 個 undo 段。
undo 段可以分為 4 種類型:臨時表 insert undo 段、臨時表 update undo 段、普通表 insert undo 段、普通表 update undo 段。
如果 undo 段中只有 1 個 undo 頁,并且 undo 頁中已使用空間小于 undo 頁大小的四分之三,undo 段可以被緩存起來復(fù)用。
可以復(fù)用的 insert undo 段緩存到 insert_undo_cached 鏈表,可用復(fù)用的 update undo 段緩存到 update_undo_cached 鏈表。
每個 undo 段都會關(guān)聯(lián)一個 inode,用于管理段中的頁,inode 存放于表空間的 inode 頁中。
一個事務(wù)產(chǎn)生的一條或多條 undo 日志會形成一個日志組,日志組由 undo log header 負(fù)責(zé)管理。
多條 undo 日志通過日志中的 next_record_start、record_start 形成雙向鏈表。
寫 undo 日志時,如果復(fù)用的 update undo 段的段頭頁中剩余空間不夠存放一條 undo 日志時,會分配一個新的 undo 頁,并把 undo 日志寫入到新的 undo 頁中。
本文轉(zhuǎn)載自微信公眾號「一樹一溪」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系一樹一溪公眾號。