一篇文章帶你了解MySQL之undo日志
一、事務(wù)回滾的需求
我們?cè)谇斑厡W(xué)習(xí)事務(wù)的時(shí)候說過事務(wù)需要保證原子性,也就是事務(wù)中的操作要么全做,要么全不做。但是有的時(shí)候事務(wù)會(huì)出現(xiàn)一些情況,比如:
- 情況一:事務(wù)執(zhí)行過程中可能遇到各種錯(cuò)誤,比如服務(wù)器本身的錯(cuò)誤,操作系統(tǒng)錯(cuò)誤,甚至是突然斷電導(dǎo)致的錯(cuò)誤
- 情況二:程序員可以在事務(wù)執(zhí)行過程中手動(dòng)輸入
ROLLBACK
語句結(jié)束當(dāng)前的事務(wù)的執(zhí)行
以上這兩種情況都會(huì)導(dǎo)致事務(wù)執(zhí)行到一半就結(jié)束,但是事務(wù)執(zhí)行過程中可能已經(jīng)修改了很多東西,為了保證事務(wù)的原子性,我們需要把東西改回原先的樣子,這個(gè)過程就稱之為回滾(英文名:rollback
),這樣就可以造成一個(gè)假象:這個(gè)事務(wù)看起來什么都沒做,所以符合原子性要求。
好比小時(shí)候我們和小伙伴們完紙牌?;谂凭褪且环N非常典型的回滾操作,比如你出兩張三紙牌,悔牌對(duì)應(yīng)的操作就是把兩張三紙牌在拿出去。數(shù)據(jù)庫中的回滾跟悔牌差不多,你插入了一條記錄,回滾操作對(duì)應(yīng)的就是把這條記錄刪除掉;你更新了一條記錄,回滾操作對(duì)應(yīng)的就是把該記錄更新為舊值;你刪除了一條記錄,回滾操作對(duì)應(yīng)的自然就是把該記錄再插進(jìn)去。說的貌似很簡單的樣子
從上邊的描述中我們已經(jīng)能隱約感覺到,每當(dāng)我們要對(duì)一條記錄做改動(dòng)時(shí)(這里的改動(dòng)可以指INSERT
、DELETE
、UPDATE
),都需要留一手 —— 把回滾時(shí)所需的東西都給記下來。比如說:
- 你插入一條記錄時(shí),至少要把這條記錄的主鍵值記下來,之后回滾的時(shí)候只需要把這個(gè)主鍵值對(duì)應(yīng)的記錄刪掉就好了
- 你刪除了一條記錄,至少要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時(shí)再把由這些內(nèi)容組成的記錄插入到表中就好了
- 你修改了一條記錄,至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時(shí)再把這條記錄更新為舊值就好了
數(shù)據(jù)庫這些為了回滾?記錄的這些東東稱之為撤銷日志,英文名為undo log
,我們稱之為undo日志
。這里需要注意的一點(diǎn)是,由于查詢操作(SELECT)并不會(huì)修改任何?戶記錄,所以在查詢操作執(zhí)行時(shí),并不需要記錄相應(yīng)的undo
日志。在真實(shí)的InnoDB
中,undo
日志其實(shí)并不像我們上邊所說的那么簡單,不同類型的操作產(chǎn)?的undo
日志的格式也是不同的,不過先暫時(shí)把這些容易讓人腦子糊的具體細(xì)節(jié)放一放,我們先回過頭來看看事務(wù)id是什么東西
二、事務(wù)id
2.1 給事務(wù)分配id的時(shí)機(jī)
我們前邊在學(xué)習(xí)事務(wù)簡介時(shí)說過,一個(gè)事務(wù)可以是一個(gè)只讀事務(wù),或者是一個(gè)讀寫事務(wù):
我們可以通過START TRANSACTION READ ONLY
語句開啟一個(gè)只讀事務(wù),在只讀事務(wù)中不可以對(duì)普通的表(其他事務(wù)也能訪問到的表)進(jìn)行增、刪、改操作,但可以對(duì)臨時(shí)表做增、刪、改操作我們可以通過START TRANSACTION READ WRITE
語句開啟一個(gè)讀寫事務(wù),或者使用BEGIN
、START TRANSACTION
語句開啟的事務(wù)默認(rèn)也算是讀寫事務(wù),在讀寫事務(wù)中可以對(duì)表執(zhí)行增刪改查操作。
如果某個(gè)事務(wù)執(zhí)行過程中對(duì)某個(gè)表執(zhí)行了增、刪、改操作,那么InnoDB
存儲(chǔ)引擎就會(huì)給它分配一個(gè)獨(dú)一無二的事務(wù)id
,分配如式如下:
對(duì)于只讀事務(wù)來說,只有在它第一次對(duì)某個(gè)用戶創(chuàng)建的臨時(shí)表執(zhí)行增、刪、改操作時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù)id,否則的話是不分配事務(wù)id的
小提示:
我們前邊說過對(duì)某個(gè)查詢語句執(zhí)行EXPLAIN
分析它的查詢計(jì)劃時(shí),有時(shí)候在Extra列會(huì)看到Using temporary的提示,這個(gè)表明在執(zhí)行該查詢語句時(shí)會(huì)用到內(nèi)部臨時(shí)表。這個(gè)所謂的內(nèi)部臨時(shí)表和我們手動(dòng)用CREATE TEMPORARY TABLE
創(chuàng)建的用戶臨時(shí)表并不一樣,在事務(wù)回滾時(shí)并不需要把執(zhí)行SELECT語句過程中用到的內(nèi)部臨時(shí)表也回滾,在執(zhí)行SELECT語句用到內(nèi)部臨時(shí)表時(shí)并不會(huì)為它分配事務(wù)id。
對(duì)于讀寫事務(wù)來說,只有在它第一次對(duì)某個(gè)表(包括用戶創(chuàng)建的臨時(shí)表)執(zhí)行增、刪、改操作時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù)id,否則的話也是不分配事務(wù)id的
有的時(shí)候雖然我們開啟了一個(gè)讀寫事務(wù),但是在這個(gè)事務(wù)中全是查詢語句,并沒有執(zhí)行增、刪、改的語句,那也就意味著這個(gè)事務(wù)并不會(huì)被分配一個(gè)事務(wù)id
說了半天,事務(wù)id有啥子用?這個(gè)先保密哈,后邊會(huì)一步步的詳細(xì)嘮叨?,F(xiàn)在只要知道只有在事務(wù)對(duì)表中的記錄做改動(dòng)時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)唯一的事務(wù)id。
2.2 事務(wù)id是怎么生成的
這個(gè)事務(wù)id本質(zhì)上就是一個(gè)數(shù)字,它的分配策略和我們前邊提到的對(duì)隱藏列row_id
(當(dāng)用戶沒有為表創(chuàng)建主鍵和UNIQUE鍵時(shí)InnoDB自動(dòng)創(chuàng)建的列)的分配策略大抵相同,具體策略如下:
- 服務(wù)器會(huì)在內(nèi)存中維護(hù)一個(gè)全局變量,每當(dāng)需要為某個(gè)事務(wù)分配一個(gè)事務(wù)id時(shí),就會(huì)把該變量的值當(dāng)作事務(wù)id分配給該事務(wù),并且把該變量自增1
- 每當(dāng)這個(gè)變量的值為256的倍數(shù)時(shí),就會(huì)將該變量的值刷新到系統(tǒng)表空間的?號(hào)為5的??中一個(gè)稱之為Max Trx ID的屬性處,這個(gè)屬性占用8個(gè)字節(jié)的存儲(chǔ)空間
- 當(dāng)系統(tǒng)下一次重新啟動(dòng)時(shí),會(huì)將上邊提到的Max Trx ID屬性加載到內(nèi)存中,將該值加上256之后賦值給我們前邊提到的全局變量(因?yàn)樵谏洗侮P(guān)機(jī)時(shí)該全局變
量的值可能大于Max Trx ID屬性值)
這樣就可以保證整個(gè)系統(tǒng)中分配的事務(wù)id值是一個(gè)遞增的數(shù)字。先被分配id的事務(wù)得到的是較小的事務(wù)id,后被分配id的事務(wù)得到的是較大的事務(wù)id。
2.3 trx_id隱藏列
我們前邊學(xué)習(xí)InnoDB
記錄行格式的時(shí)候重點(diǎn)強(qiáng)調(diào)過:聚簇索引的記錄除了會(huì)保存完整的用戶數(shù)據(jù)以外,而且還會(huì)自動(dòng)添加名為trx_id
、roll_pointer
的隱藏列,如果用戶沒有在表中定義主鍵以及UNIQUE鍵,還會(huì)自動(dòng)添加一個(gè)名為row_id
的隱藏列。所以一條記錄在頁面中的真實(shí)結(jié)構(gòu)看起來就是這樣的:
其中的trx_id
列其實(shí)還蠻好理解的,就是某個(gè)對(duì)這個(gè)聚簇索引記錄做改動(dòng)的語句所在的事務(wù)對(duì)應(yīng)的事務(wù)id而已(此處的改動(dòng)可以是INSERT
、DELETE
、UPDATE
操作)。至于roll_pointer
隱藏列我們后邊分析~
三、undo日志的格式
為了實(shí)現(xiàn)事務(wù)的原子性,InnoDB
存儲(chǔ)引擎在實(shí)際進(jìn)行增、刪、改一條記錄時(shí),都需要先把對(duì)應(yīng)的undo
日志記下來。一般每對(duì)一條記錄做一次改動(dòng),就對(duì)應(yīng)著一條undo
日志,但在某些更新記錄的操作中,也可能會(huì)對(duì)應(yīng)著2條undo
日志,這個(gè)我們后邊會(huì)仔細(xì)嘮叨。一個(gè)事務(wù)在執(zhí)行過程中可能新增、刪除、更新若干條記錄,也就是說需要記錄很多條對(duì)應(yīng)的undo
日志,這些undo
日志會(huì)被從0
開始編號(hào),也就是說根據(jù)生成的順序分別被稱為第0號(hào)undo日志、第1號(hào)undo日志、…、第n號(hào)undo日志等,這個(gè)編號(hào)也被稱之為undo no
。
這些undo日志是被記錄到類型為FIL_PAGE_UNDO_LOG
(對(duì)應(yīng)的十六進(jìn)制是0x0002
,忘記了頁面類型是個(gè)啥的同學(xué)需要回過頭再看看前邊的章節(jié))的頁面中。這些頁面可以從系統(tǒng)表空間中分配,也可以從一種專門存放undo日志的表空間,也就是所謂的undo tablespace
中分配。不過關(guān)于如何分配存儲(chǔ)undo
日志的頁面這個(gè)事情我們稍后再說,現(xiàn)在先來看看不同操作都會(huì)產(chǎn)生什么樣子的undo
日志吧~ 為了故事的順利發(fā)展,我們先來創(chuàng)建一個(gè)名為demo18
的表:
mysql> CREATE TABLE demo18 ( id INT NOT NULL, key1 VARCHAR(100), col VARCHAR(100), PRIMARY KEY (id), KEY idx_key1 (key1) )Engine=InnoDB CHARSET=utf8; Query OK, 0 rows affected, 1 warning (0.06 sec)
這個(gè)表中有3個(gè)列,其中id
列是主鍵,我們?yōu)?code>key1列建立了一個(gè)二級(jí)索引,col列是一個(gè)普通的列。我們前邊介紹InnoDB
的數(shù)據(jù)字典時(shí)說過,每個(gè)表都會(huì)被分配一個(gè)唯一的table id
,我們可以通過系統(tǒng)數(shù)據(jù)庫information_schema
中的innodb_tables
表來查看某個(gè)表對(duì)應(yīng)的table id
是什么,現(xiàn)在我們查看一下demo18
對(duì)應(yīng)的table id
是多少:
mysql> SELECT * FROM information_schema.innodb_tables WHERE name = 'testdb/demo18'; +----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+ | TABLE_ID | NAME | FLAG | N_COLS | SPACE | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE | INSTANT_COLS | TOTAL_ROW_VERSIONS | +----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+ | 1128 | testdb/demo18 | 33 | 6 | 66 | Dynamic | 0 | Single | 0 | 0 | +----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+ 1 row in set (0.00 sec)
從查詢結(jié)果可以看出,demo18
表對(duì)應(yīng)的table id
為1128
,先把這個(gè)值記住,我們后邊有用
3.1 INSERT操作對(duì)應(yīng)的undo日志
我們前邊說過,當(dāng)我們向表中插入一條記錄時(shí)會(huì)有樂觀插入和悲觀插入的區(qū)分,但是不管怎么插入,最終導(dǎo)致的結(jié)果就是這條記錄被放到了一個(gè)數(shù)據(jù)頁中。如果希望回滾這個(gè)插入操作,那么把這條記錄刪除就好了,也就是說在寫對(duì)應(yīng)的undo
日志時(shí),主要是把這條記錄的主鍵信息記上。所以InnoDB
設(shè)計(jì)了一個(gè)類型為TRX_UNDO_INSERT_REC
的undo
日志,它的完整結(jié)構(gòu)如下圖所示:
根據(jù)示意圖我們強(qiáng)調(diào)幾點(diǎn):
undo no
在一個(gè)事務(wù)中是從0開始遞增的,也就是說只要事務(wù)沒提交,每生成一條undo日志,那么該條日志的undo no就增1。
如果記錄中的主鍵只包含一個(gè)列,那么在類型為TRX_UNDO_INSERT_REC
的undo
日志中只需要把該列占用的存儲(chǔ)空間大小和真實(shí)值記錄下來,如果記錄中的主鍵包含多個(gè)列,那么每個(gè)列占用的存儲(chǔ)空間大小和對(duì)應(yīng)的真實(shí)值都需要記錄下來(圖中的len
就代表列占用的存儲(chǔ)空間大小,value
就代表列的真實(shí)值)。
小提示:
當(dāng)我們向某個(gè)表中插入一條記錄時(shí),實(shí)際上需要向聚簇索引和所有的二級(jí)索引都插入一條記錄。不過記錄undo日志時(shí),我們只需要考慮向聚簇索引插入記錄時(shí)的情況就好了,因?yàn)槠鋵?shí)聚簇索引記錄和二級(jí)索引記錄是一一對(duì)應(yīng)的,我們?cè)诨貪L插入操作時(shí),只需要知道這條記
錄的主鍵信息,然后根據(jù)主鍵信息做對(duì)應(yīng)的刪除操作,做刪除操作時(shí)就會(huì)順帶著把所有二級(jí)索引中相應(yīng)的記錄也刪除掉。后邊說到的DELETE操作和UPDATE操作對(duì)應(yīng)的undo日志也都是針對(duì)聚簇索引記錄而?的,我們之后就不強(qiáng)調(diào)了。
現(xiàn)在我們向demo18中插入兩條記錄:
mysql> BEGIN; # 顯式開啟一個(gè)事務(wù),假設(shè)該事務(wù)的id為100 Query OK, 0 rows affected (0.00 sec) mysql> # 插入兩條記錄 mysql> INSERT INTO demo18(id, key1, col) VALUES (1, 'AWM', '狙擊槍'), (2, 'M416', '步槍'); Query OK, 2 rows affected (0.01 sec) Records: 2 Duplicates: 0 Warnings: 0
因?yàn)橛涗浀闹麈I只包含一個(gè)id
列,所以我們?cè)趯?duì)應(yīng)的undo
日志中只需要將待插入記錄的id
列占用的存儲(chǔ)空間長度(id列的類型為INT,INT類型占用的存儲(chǔ)空間長度為4個(gè)字節(jié))和真實(shí)值
記錄下來。本例中插入了兩條記錄,所以會(huì)產(chǎn)生兩條類型為TRX_UNDO_INSERT_REC
的undo
日志:
第一條undo
日志的undo no
為0
,記錄主鍵占用的存儲(chǔ)空間長度為4
,真實(shí)值為1
。畫一個(gè)示意圖就是這樣:
第二條undo
日志的undo no
為1
,記錄主鍵占用的存儲(chǔ)空間長度為4
,真實(shí)值為2
。畫一個(gè)示意圖就是這樣:
與第一條undo日志
對(duì)比,undo no
和主鍵各列信息有不同。
roll_pointer隱藏列的含義
是時(shí)候揭開roll_pointer
的真實(shí)面紗了,這個(gè)占用7
個(gè)字節(jié)的字段其實(shí)一點(diǎn)都不神秘,本質(zhì)上就是一個(gè)指向記錄對(duì)應(yīng)的undo日志的一個(gè)指針
。比如說我們上邊向demo18
表里插入了2
條記錄,每條記錄都有與其對(duì)應(yīng)的一條undo日志
。記錄被存儲(chǔ)到了類型為FIL_PAGE_INDEX
的頁面中(就是我們前邊一直所說的數(shù)據(jù)頁),undo
日志被存放到了類型為FIL_PAGE_UNDO_LOG
的頁面中。效果如圖所示:
從圖中也可以更直觀的看出來,roll_pointer
本質(zhì)就是一個(gè)指針,指向記錄對(duì)應(yīng)的undo
日志。不過這7個(gè)字節(jié)的roll_pointer
的每一個(gè)字節(jié)具體的含義我們后邊嘮叨完如何分配存儲(chǔ)undo
日志的頁面之后再具體說哈~
3.2 DELETE操作對(duì)應(yīng)的undo日志
我們知道插入到頁面中的記錄會(huì)根據(jù)記錄頭信息中的next_record
屬性組成一個(gè)單向鏈表,我們把這個(gè)鏈表稱之為正常記錄鏈表;我們?cè)谇斑厙Z叨數(shù)據(jù)頁結(jié)構(gòu)的時(shí)候說過,被刪除的記錄其實(shí)也會(huì)根據(jù)記錄頭信息中的next_record
屬性組成一個(gè)鏈表,只不過這個(gè)鏈表中的記錄占用的存儲(chǔ)空間可以被重新利用,所以也稱這個(gè)鏈表為垃圾鏈表
。PageHeader
部分有一個(gè)稱之為PAGE_FREE
的屬性,它指向由被刪除記錄組成的垃圾鏈表中的頭節(jié)點(diǎn)。為了故事的順利發(fā)展,我們先畫一個(gè)圖,假設(shè)此刻某個(gè)頁面中的記錄分布情況是這樣的(這個(gè)不是demo18
表中的記錄,只是我們隨便舉的一個(gè)例子):
為了突出主題,在這個(gè)簡化版的示意圖中,我們只把記錄的delete_mask
標(biāo)志位展示了出來。從圖中可以看出,正常記錄鏈表中包含了3條正常記錄,垃圾鏈表里包含了2條已刪除記錄,在垃圾鏈表中的這些記錄占用的存儲(chǔ)空間可以被重新利用。頁面的Page Header
部分的PAGE_FREE
屬性的值代表指向垃圾鏈表頭節(jié)點(diǎn)的指針。假設(shè)現(xiàn)在我們準(zhǔn)備使用DELETE
語句把正常記錄鏈表中的最后一條記錄給刪除掉,其實(shí)這個(gè)刪除的過程需要經(jīng)歷兩個(gè)階段:
階段一:僅僅將記錄的delete_mask
標(biāo)識(shí)位設(shè)置為1
,其他的不做修改(其實(shí)會(huì)修改記錄的trx_id
、roll_pointer
這些隱藏列的值)。InnoDB把這個(gè)階段稱之為delete mar
k。把這個(gè)過程畫下來就是這樣:
可以看到,正常記錄鏈表中的最后一條記錄的delete_mask值被設(shè)置為1,但是并沒有被加入到垃圾鏈表。也就是此時(shí)記錄處于一個(gè)中間狀態(tài),在刪除語句所在的事務(wù)提交之前,被刪除的記錄一直都處于這種所謂的中間狀態(tài)。
小提示:
為啥會(huì)有這種奇怪的中間狀態(tài)呢?其實(shí)主要是為了實(shí)現(xiàn)一個(gè)稱之為MVCC的功能,哈哈,稍后再介紹。
階段二:當(dāng)該刪除語句所在的事務(wù)提交之后,會(huì)有專門的線程后來真正的把記錄刪除掉。所謂真正的刪除就是把該記錄從正常記錄鏈表中移除,并且加入到垃圾鏈表中,然后還要調(diào)整一些頁面的其他信息,比如頁面中的用戶記錄數(shù)量PAGE_N_RECS
、上次插入記錄的位置PAGE_LAST_INSERT
、垃圾鏈表頭節(jié)點(diǎn)的指針PAGE_FREE
、頁面中可重用的字節(jié)數(shù)量PAGE_GARBAGE
、還有頁?錄的一些信息等等。InnoDB的把這個(gè)階段稱之為purge
。
把階段二執(zhí)行完了,這條記錄就算是真正的被刪除掉了。這條已刪除記錄占用的存儲(chǔ)空間也可以被重新利用了。畫下來就是這樣:
對(duì)照著圖我們還要注意一點(diǎn),將被刪除記錄加入到垃圾鏈表時(shí),實(shí)際上加入到鏈表的頭節(jié)點(diǎn)處,會(huì)跟著修改PAGE_FREE
屬性的值。
小提示:
頁面的Page Header部分有一個(gè)PAGE_GARBAGE屬性,該屬性記錄著當(dāng)前頁面中可重用存儲(chǔ)空間占用的總字節(jié)數(shù)。每當(dāng)有已刪除記錄被加入到垃圾鏈表后,都會(huì)把這個(gè)PAGE_GARBAGE屬性的值加上該已刪除記錄占用的存儲(chǔ)空間大小。PAGE_FREE指向垃圾鏈表的頭節(jié)點(diǎn),之后每當(dāng)新插入記錄時(shí),?先判斷PAGE_FREE指向的頭節(jié)點(diǎn)代表的已刪除記錄占用的存儲(chǔ)空間是否足夠容納這條新插入的記錄,如果不可以容納,就直接向頁面中申請(qǐng)新的空間來存儲(chǔ)這條記錄(是的,你沒看錯(cuò),并不會(huì)嘗試遍歷整個(gè)垃圾鏈表,找到一個(gè)可以容納新記錄的節(jié)點(diǎn))。如果可以容納,那么直接重用這條已刪除記錄的存儲(chǔ)空間,并且把PAGE_FREE指向垃圾鏈表中的下一條已刪除記錄。但是這里有一個(gè)問題,如果新插入的那條記錄占用的存儲(chǔ)空間大小小于垃圾鏈表的頭節(jié)點(diǎn)占用的存儲(chǔ)空間大小,那就意味頭節(jié)點(diǎn)對(duì)應(yīng)的記錄占用的存儲(chǔ)空間里有一部分空間用不到,這部分空間就被稱之為碎片空間。那這些碎片空間豈不是永遠(yuǎn)都用不到了么?其實(shí)也不是,這些碎片空間占用的存儲(chǔ)空間大小會(huì)被統(tǒng)計(jì)到PAGE_GARBAGE屬性中,這些碎片空間在整個(gè)頁面快使用完前并不會(huì)被重新利用,不過當(dāng)頁面快滿時(shí),如果再插入一條記錄,此時(shí)頁面中并不能分配一條完整記錄的空間,這時(shí)候會(huì)?先看一看PAGE_GARBAGE的空間和剩余可利用的空間加起來是不是可以容納下這條記錄,如果可以的話,InnoDB會(huì)嘗試重新組織頁內(nèi)的記錄,重新組織的過程就是先開辟一個(gè)臨時(shí)頁面,把頁面內(nèi)的記錄依次插入一遍,因?yàn)橐来尾迦霑r(shí)并不會(huì)產(chǎn)生碎片,之后再把臨時(shí)頁面的內(nèi)容復(fù)制到本頁面,這樣就可以把那些碎片空間都解放出來(很顯然重新組織頁面內(nèi)的記錄比較耗費(fèi)性能)。
從上邊的描述中我們也可以看出來,在刪除語句所在的事務(wù)提交之前,只會(huì)經(jīng)歷階段一,也就是delete mark
階段(提交之后我們就不用回滾了,所以只需考慮對(duì)刪除操作的階段一做的影響進(jìn)行回滾)。InnoDB
為此設(shè)計(jì)了一種稱之為TRX_UNDO_DEL_MARK_REC
類型的undo
日志,它的完整結(jié)構(gòu)如下圖所示:
額滴個(gè)神吶,這個(gè)里邊的屬性也太多了點(diǎn)兒吧~ (其實(shí)大部分屬性的意思我們上邊已經(jīng)介紹過了) 是的,的確有點(diǎn)多,不過大家千萬不要在意,如果記不住千萬不要勉強(qiáng)自已,我這里把它們都列出來讓大家混個(gè)臉熟而已。勞煩大家先克服一下密集恐急癥,再抬頭大致看一遍上邊的這個(gè)類型為TRX_UNDO_DEL_MARK_REC
的undo
日志中的屬性,特別注意一下這幾點(diǎn):
在對(duì)一條記錄進(jìn)行delete mark
操作前,需要把該記錄的舊的trx_id
和roll_pointer
隱藏列的值都給記到對(duì)應(yīng)的undo
日志中來,就是我們圖中顯示的oldtrx_id
和old roll_pointer
屬性。這樣有一個(gè)好處,那就是可以通過undo
日志的old roll_pointer
找到記錄在修改之前對(duì)應(yīng)的undo
日志。比如說在一個(gè)事務(wù)中,我們先插入了一條記錄,然后又執(zhí)行對(duì)該記錄的刪除操作,這個(gè)過程的示意圖就是這樣:
從圖中可以看出來,執(zhí)行完delete mark
操作后,它對(duì)應(yīng)的undo
日志和INSERT
操作對(duì)應(yīng)的undo
日志就串成了一個(gè)鏈表。這個(gè)很有意思啊,這個(gè)鏈表就稱之為版本鏈,現(xiàn)在貌似看不出這個(gè)版本鏈有啥用,等我們?cè)偻罂纯?,講完UPDATE
操作對(duì)應(yīng)的undo
日志后,這個(gè)所謂的版本鏈就慢慢的展現(xiàn)出它的?逼之處了。
與類型為TRX_UNDO_INSERT_REC
的undo
日志不同,類型為TRX_UNDO_DEL_MARK_REC
的undo
日志還多了一個(gè)索引列各列信息
的內(nèi)容,也就是說如果某個(gè)列被包含在某個(gè)索引中,那么它的相關(guān)信息就應(yīng)該被記錄到這個(gè)索引列各列信息
部分,所謂的相關(guān)信息包括該列在記錄中的位置(用pos
表示),該列占用的存儲(chǔ)空間大?。ㄓ?code>len表示),該列實(shí)際值(用value
表示)。所以索引列各列信息
存儲(chǔ)的內(nèi)容實(shí)質(zhì)上就是<pos, len, value>
的一個(gè)列表。這部分信息主要是用在事務(wù)提交后,對(duì)該中間狀態(tài)記錄
做真正刪除的階段二,也就是purge
階段中使用的,具體如何使用現(xiàn)在我們可以忽略~
該介紹的我們介紹完了,現(xiàn)在繼續(xù)在上邊那個(gè)事務(wù)id
為100
的事務(wù)中刪除一條記錄,比如我們把id
為1
的那條記錄刪除掉:
mysql> DELETE FROM demo18 WHERE id = 1; Query OK, 1 row affected (0.01 sec)
這個(gè)delete mark
操作對(duì)應(yīng)的undo
日志的結(jié)構(gòu)就是這樣:
對(duì)照著這個(gè)圖,我們得注意下邊幾點(diǎn):
因?yàn)檫@條undo日志是id為100的事務(wù)中產(chǎn)生的第3條undo日志,所以它對(duì)應(yīng)的undo no就是2。
在對(duì)記錄做delete mark
操作時(shí),記錄的trx_id
隱藏列的值是100
(也就是說對(duì)該記錄最近的一次修改就發(fā)生在本事務(wù)中),所以把100
填入old trx_id
屬性中。然后把記錄的roll_pointer
隱藏列的值取出來,填入old roll_pointer
屬性中,這樣就可以通過old roll_pointer
屬性值找到最近一次對(duì)該記錄做改動(dòng)時(shí)產(chǎn)生的undo
日志。
由于demo18表中有2個(gè)索引:一個(gè)是聚簇索引,一個(gè)是二級(jí)索引idx_key1
。只要是包含在索引中的列,那么這個(gè)列在記錄中的位置(pos
),占用存儲(chǔ)空間大?。?code>len)和實(shí)際值(value
)就需要存儲(chǔ)到undo日志中。
- 對(duì)于主鍵來說,只包含一個(gè)
id
列,存儲(chǔ)到undo日志中的相關(guān)信息分別是:pos
:id
列是主鍵,也就是在記錄的第一個(gè)列
,它對(duì)應(yīng)的pos值為0。pos占用1個(gè)字節(jié)來存儲(chǔ)。len
:id列的類型為INT,占用4個(gè)字節(jié),所以len的值為4。len占用1個(gè)字節(jié)來存儲(chǔ)。value
:在被刪除的記錄中id列的值為1,也就是value的值為1。value占用4個(gè)字節(jié)來存儲(chǔ)。- 畫一個(gè)圖演示一下就是這樣:
- 所以對(duì)于
id
列來說,最終存儲(chǔ)的結(jié)果就是<0, 4, 1
>,存儲(chǔ)這些信息占用的存儲(chǔ)空間大小為1 + 1 + 4 = 6個(gè)字節(jié)
。 - 對(duì)于
idx_key1
來說,只包含一個(gè)key1
列,存儲(chǔ)到undo
日志中的相關(guān)信息分別是:pos
:key1列是排在id列、trx_id列、roll_pointer列之后的,它對(duì)應(yīng)的pos值為3
。pos占用1個(gè)字節(jié)來存儲(chǔ)。len
:key1列的類型為VARCHAR(100),使用utf8字符集,被刪除的記錄實(shí)際存儲(chǔ)的內(nèi)容是AWM,所以一共占用3個(gè)字節(jié),也就是所以len的值為3。len占用1個(gè)字節(jié)來存儲(chǔ)。value
:在被刪除的記錄中key1列的值為AWM,也就是value的值為AWM。value占用3個(gè)字節(jié)來存儲(chǔ)。
- 畫一個(gè)圖演示一下就是這樣:
- 所以對(duì)于
key1
列來說,最終存儲(chǔ)的結(jié)果就是<3, 3, 'AWM'
>,存儲(chǔ)這些信息占用的存儲(chǔ)空間大小為1 + 1 + 3 = 5
個(gè)字節(jié)。
從上邊的敘述中可以看到,<0, 4, 1>
和<3, 3, 'AWM'>
共占用11
個(gè)字節(jié)。然后index_col_info len
本身占用2
個(gè)字節(jié),所以加起來一共占用13
個(gè)字節(jié),把數(shù)字13就填到了index_col_info len
的屬性中。
3.3 UPDATE操作對(duì)應(yīng)的undo日志
在執(zhí)行UPDATE
語句時(shí),InnoDB對(duì)更新主鍵和不更新主鍵這兩種情況有截然不同的處理如案。
3.3.1 不更新主鍵的情況
在不更新主鍵的情況下,又可以細(xì)分為被更新的列占用的存儲(chǔ)空間不發(fā)生變化和發(fā)生變化的情況。
就地更新(in-place update)
更新記錄時(shí),對(duì)于被更新的每個(gè)列來說,如果更新后的列和更新前的列占用的存儲(chǔ)空間都一樣大,那么就可以進(jìn)行就地更新,也就是直接在原記錄的基礎(chǔ)上修改對(duì)應(yīng)列的值。再次強(qiáng)調(diào)一邊,是每個(gè)列在更新前后占用的存儲(chǔ)空間一樣大,有任何一個(gè)被更新的列更新前比更新后占用的存儲(chǔ)空間大,或者更新前比更新后占用的存儲(chǔ)空間小都不能進(jìn)行就地更新。比如說現(xiàn)在demo18表里還有一條id值為2的記錄,它的各個(gè)列占用的大小如圖所示(因?yàn)椴捎胾tf8字符集,所以’步槍’這兩個(gè)字符占用6個(gè)字節(jié)):
假如我們有這樣的UPDATE
語句:
UPDATE demo18 SET key1 = 'P92', col = '手槍' WHERE id = 2;
在這個(gè)UPDATE語句中,col列從步槍被更新為手槍,前后都占用6個(gè)字節(jié),也就是占用的存儲(chǔ)空間大小未改變;key1列從M416被更新為P92,也就是從4個(gè)字節(jié)被更新為3個(gè)字節(jié),這就不滿足就地更新需要的條件了,所以不能進(jìn)行就地更新。但是如果UPDATE語句長這樣:
UPDATE demo18 SET key1 = 'M249', col = '機(jī)槍' WHERE id = 2;
由于各個(gè)被更新的列在更新前后占用的存儲(chǔ)空間是一樣大的,所以這樣的語句可以執(zhí)行就地更新。
先刪除掉舊記錄,再插入新記錄
在不更新主鍵的情況下,如果有任何一個(gè)被更新的列更新前和更新后占用的存儲(chǔ)空間大小不一致,那么就需要先把這條舊的記錄從聚簇索引頁面中刪除掉,然后再根據(jù)更新后列的值創(chuàng)建一條新的記錄插入到頁面中。
請(qǐng)注意一下,我們這里所說的刪除并不是delete mark
操作,而是真正的刪除掉,也就是把這條記錄從正常記錄鏈表中移除并加入到垃圾鏈表中,并且修改頁面中相應(yīng)的統(tǒng)計(jì)信息(比如PAGE_FREE
、PAGE_GARBAGE
等這些信息)。不過這里做真正刪除操作的線程并不是在嘮叨DELETE
語句中做purge
操作時(shí)使用的另外專門的線程,而是由用戶線程同步執(zhí)行真正的刪除操作,真正刪除之后緊接著就要根據(jù)各個(gè)列更新后的值創(chuàng)建的新記錄插入。
這里如果新創(chuàng)建的記錄占用的存儲(chǔ)空間大小不超過舊記錄占用的空間,那么可以直接重用被加入到垃圾鏈表中的舊記錄所占用的存儲(chǔ)空間,否則的話需要在頁面中新申請(qǐng)一段空間以供新記錄使用,如果本頁面內(nèi)已經(jīng)沒有可用的空間的話,那就需要進(jìn)行頁面分裂操作,然后再插入新記錄。
針對(duì)UPDATE不更新主鍵的情況(包括上邊所說的就地更新和先刪除舊記錄再插入新記錄),InnoDB設(shè)計(jì)了一種類型為TRX_UNDO_UPD_EXIST_REC
的undo日志,它的完整結(jié)構(gòu)如下:
其實(shí)大部分屬性和我們介紹過的TRX_UNDO_DEL_MARK_REC
類型的undo日志是類似的,不過還是要注意這么幾點(diǎn):
- n_updated屬性表示本條UPDATE語句執(zhí)行后將有幾個(gè)列被更新,后邊跟著的<pos, old_len, old_value>分別表示被更新列在記錄中的位置、更新前該列占用的存儲(chǔ)空間大小、更新前該列的真實(shí)值。
- 如果在UPDATE語句中更新的列包含索引列,那么也會(huì)添加索引列各列信息這個(gè)部分,否則的話是不會(huì)添加這個(gè)部分的。
現(xiàn)在繼續(xù)在上邊那個(gè)事務(wù)id
為100
的事務(wù)中更新一條記錄,比如我們把id
為2
的那條記錄更新一下:
BEGIN; # 顯式開啟一個(gè)事務(wù),假設(shè)該事務(wù)的id為100 # 插入兩條記錄 INSERT INTO demo18(id, key1, col) VALUES (1, 'AWM', '狙擊槍'), (2, 'M416', '步槍'); # 刪除一條記錄 DELETE FROM demo18 WHERE id = 1; # 更新一條記錄 UPDATE demo18 SET key1 = 'M249', col = '機(jī)槍' WHERE id = 2;
這個(gè)UPDATE
語句更新的列大小都沒有改動(dòng),所以可以采用就地更新
的如式來執(zhí)行,在真正改動(dòng)頁面記錄時(shí),會(huì)先記錄一條類型為TRX_UNDO_UPD_EXIST_REC
的undo日志,長這樣:
對(duì)照著這個(gè)圖我們注意一下這幾個(gè)地如:
- 因?yàn)檫@條undo日志是id為100的事務(wù)中產(chǎn)生的第4條undo日志,所以它對(duì)應(yīng)的undo no就是3。
- 這條日志的roll_pointer指向undo no為1的那條日志,也就是插入主鍵值為2的記錄時(shí)產(chǎn)生的那條undo日志,也就是最近一次對(duì)該記錄做改動(dòng)時(shí)產(chǎn)生的undo日志。
- 由于本條UPDATE語句中更新了索引列key1的值,所以需要記錄一下索引列各列信息部分,也就是把主鍵和key1列更新前的信息填入。
3.3.2 更新主鍵的情況
在聚簇索引中,記錄是按照主鍵值的大小連成了一個(gè)單向鏈表的,如果我們更新了某條記錄的主鍵值,意味著這條記錄在聚簇索引中的位置將會(huì)發(fā)生改變,比如你將記錄的主鍵值從1更新為10000,如果還有非常多的記錄的主鍵值分布在1 ~ 10000之間的話,那么這兩條記錄在聚簇索引中就有可能離得非常遠(yuǎn),甚至中間隔了好多個(gè)頁面。針對(duì)UPDATE
語句中更新了記錄主鍵值的這種情況,InnoDB
在聚簇索引中分了兩步處理:
- 將舊記錄進(jìn)行delete mark操作
高能注意:這里是delete mark操作!也就是說在UPDATE
語句所在的事務(wù)提交前,對(duì)舊記錄只做一個(gè)delete mark
操作,在事務(wù)提交后才由專門的線程做purge
操作,把它加入到垃圾鏈表中。這里一定要和我們上邊所說的在不更新記錄主鍵值時(shí),先真正刪除舊記錄,再插入新記錄的如式區(qū)分開!
小提示:
之所以只對(duì)舊記錄做delete mark操作,是因?yàn)閯e的事務(wù)同時(shí)也可能訪問這條記錄,如果把它真正的刪除加入到垃圾鏈表后,別的事務(wù)就訪問不到了。這個(gè)功能就是所謂的MVCC,我們后邊的章節(jié)中會(huì)詳細(xì)嘮叨什么是個(gè)MVCC。
- 根據(jù)更新后各列的值創(chuàng)建一條新記錄,并將其插入到聚簇索引中(需重新定位插入的位置)。
由于更新后的記錄主鍵值發(fā)生了改變,所以需要重新從聚簇索引中定位這條記錄所在的位置,然后把它插進(jìn)去。
針對(duì)UPDATE
語句更新記錄主鍵值的這種情況,在對(duì)該記錄進(jìn)行delete mark
操作前,會(huì)記錄一條類型為TRX_UNDO_DEL_MARK_REC
的undo日志;之后插入新記錄時(shí),會(huì)記錄一條類型為TRX_UNDO_INSERT_REC
的undo
日志,也就是說每對(duì)一條記錄的主鍵值做改動(dòng)時(shí),會(huì)記錄2條undo日志。這些日志的格式我們上邊都嘮叨過了,就不贅述了。
四、通用鏈表結(jié)構(gòu)
在寫入undo日志
的過程中會(huì)使用到多個(gè)鏈表,很多鏈表都有同樣的節(jié)點(diǎn)結(jié)構(gòu),如圖所示:
在某個(gè)表空間內(nèi),我們可以通過一個(gè)頁的頁號(hào)和在頁內(nèi)的偏移量來唯一定位一個(gè)節(jié)點(diǎn)的位置,這兩個(gè)信息也就相當(dāng)于指向這個(gè)節(jié)點(diǎn)的一個(gè)指針。所以:
Pre Node Page Number
和Pre Node Offset
的組合就是指向前一個(gè)節(jié)點(diǎn)的指針Next Node Page Number
和Next Node Offset
的組合就是指向后一個(gè)節(jié)點(diǎn)的指針。
整個(gè)List Node
占用12個(gè)字節(jié)的存儲(chǔ)空間。為了更好的管理鏈表,InnoDB的提出了一個(gè)基節(jié)點(diǎn)的結(jié)構(gòu),里邊存儲(chǔ)了這個(gè)鏈表的頭節(jié)點(diǎn)、尾節(jié)點(diǎn)以及鏈表長度信息,基節(jié)點(diǎn)的結(jié)構(gòu)示意圖如下:
其中:
List Length
表明該鏈表一共有多少節(jié)點(diǎn)。First Node Page Number和First Node Offset
的組合就是指向鏈表頭節(jié)點(diǎn)的指針。Last Node Page Number和Last Node Offset
的組合就是指向鏈表尾節(jié)點(diǎn)的指針。
整個(gè)List Base Node
占用16
個(gè)字節(jié)的存儲(chǔ)空間。所以使用List Base Node
和List Node
這兩個(gè)結(jié)構(gòu)組成的鏈表的示意圖就是這樣:
五、 FIL_PAGE_UNDO_LOG頁面
我們前邊嘮叨表空間的時(shí)候說過,表空間其實(shí)是由許許多多的頁面構(gòu)成的,頁面默認(rèn)大小為16KB。這些頁面有不同的類型,比如類型為FIL_PAGE_INDEX
的頁面用于存儲(chǔ)聚簇索引以及二級(jí)索引,類型為FIL_PAGE_TYPE_FSP_HDR
的頁面用于存儲(chǔ)表空間頭部信息的,還有其他各種類型的頁面,其中有一種稱之為FIL_PAGE_UNDO_LOG
類型的頁面是專門用來存儲(chǔ)undo日志
的,這種類型的頁面的通用結(jié)構(gòu)如下圖所示(以默認(rèn)的16KB大小為例):
類型為FIL_PAGE_UNDO_LOG
的頁我們就簡稱為Undo
頁面。上圖中的File Header
和File Trailer
是各種頁面都有的通用結(jié)構(gòu),我們前邊已經(jīng)學(xué)習(xí)了很多遍了,這里就不贅述了。Undo Page Header
是Undo頁面
所特有的,我們來看一下它的結(jié)構(gòu):
其中各個(gè)屬性的意思如下:
TRX_UNDO_PAGE_TYPE
:本頁面準(zhǔn)備存儲(chǔ)什么種類的undo日志。
我們前邊介紹了好幾種類型的undo日志,它們可以被分為兩個(gè)大類:
TRX_UNDO_INSERT
(使用十進(jìn)制1表示):類型為TRX_UNDO_INSERT_REC
的undo日志屬于此大類,一般由INSERT
語句產(chǎn)生,或者在UPDATE
語句中有更新主鍵的情況也會(huì)產(chǎn)生此類型的undo
日志。TRX_UNDO_UPDATE
(使用十進(jìn)制2表示),除了類型為TRX_UNDO_INSERT_REC
的undo
日志,其他類型的undo
日志都屬于這個(gè)大類,比如我們前邊說的TRX_UNDO_DEL_MARK_REC
、TRX_UNDO_UPD_EXIST_REC
等,一般由DELETE
、UPDATE
語句產(chǎn)生的undo
日志屬于這個(gè)大類。
這個(gè)TRX_UNDO_PAGE_TYPE
屬性可選的值就是上邊的兩個(gè),用來標(biāo)記本頁面用于存儲(chǔ)哪個(gè)大類的undo
日志,不同大類的undo
日志不能混著存儲(chǔ),比如一個(gè)Undo
頁面的TRX_UNDO_PAGE_TYPE
屬性值為TRX_UNDO_INSERT
,那么這個(gè)頁面就只能存儲(chǔ)類型為TRX_UNDO_INSERT_REC
的undo
日志,其他類型的undo日志就不能放到這個(gè)頁面中了。
小提示:
之所以把undo日志分成兩個(gè)大類,是因?yàn)轭愋蜑門RX_UNDO_INSERT_REC的undo日志在事務(wù)提交后可以直接刪除掉,而其他類型的undo日志還需要為所謂的MVCC服務(wù),不能直接刪除掉,對(duì)它們的處理需要區(qū)別對(duì)待。當(dāng)然,如果你看這段話迷迷糊糊的話,那就不需要再看一遍了,現(xiàn)在只需要知道undo日志分為2個(gè)大類就好了,更詳細(xì)的東西我們后邊會(huì)有講解。
TRX_UNDO_PAGE_START
:表示在當(dāng)前頁面中是從什么位置開始存儲(chǔ)undo
日志的,或者說表示第一條undo
日志在本頁面中的起始偏移量。TRX_UNDO_PAGE_FREE
:與上邊的TRX_UNDO_PAGE_START
對(duì)應(yīng),表示當(dāng)前頁面中存儲(chǔ)的最后一條undo
日志結(jié)束時(shí)的偏移量,或者說從這個(gè)位置開始,可以繼續(xù)寫入新的undo日志。
假設(shè)現(xiàn)在向頁面中寫入了3條undo日志,那么TRX_UNDO_PAGE_START
和TRX_UNDO_PAGE_FREE
的示意圖就是這樣:
當(dāng)然,在最初一條undo日志也沒寫入的情況下,TRX_UNDO_PAGE_START
和TRX_UNDO_PAGE_FREE
的值是相同的。
TRX_UNDO_PAGE_NODE:代表一個(gè)List Node結(jié)構(gòu)(鏈表的普通節(jié)點(diǎn),我們上邊剛說的),下邊馬上用到這個(gè)屬性,稍安勿躁。
六、Undo頁面鏈表
6.1 單個(gè)事務(wù)中的Undo頁面鏈表
因?yàn)橐粋€(gè)事務(wù)可能包含多個(gè)語句,而且一個(gè)語句可能對(duì)若干條記錄進(jìn)行改動(dòng),而對(duì)每條記錄進(jìn)行改動(dòng)前,都需要記錄1條或2條的undo日志,所以在一個(gè)事務(wù)執(zhí)行過程中可能產(chǎn)生很多undo日志,這些日志可能一個(gè)頁面放不下,需要放到多個(gè)頁面中,這些頁面就通過我們上邊介紹的TRX_UNDO_PAGE_NODE
屬性連成了鏈表:
大家可以看一看上邊的圖,一邊情況下把鏈表中的第一個(gè)Undo頁稱它為first undo page
,因?yàn)樵?code>first undo page中除了記錄Undo Page Header
之外,還會(huì)記錄其他的一些管理信息。其余的Undo頁面稱之為normal undo page
。
在一個(gè)事務(wù)執(zhí)行過程中,可能混著執(zhí)行INSERT、DELETE
、UPDATE
語句,也就意味著會(huì)產(chǎn)生不同類型的undo日志。但是我們前邊又說過,同一個(gè)Undo
頁面要么只存儲(chǔ)TRX_UNDO_INSERT
大類的undo日志,要么只存儲(chǔ)TRX_UNDO_UPDATE
大類的undo日志,反正不能混著存,所以在一個(gè)事務(wù)執(zhí)行過程中就可能需要2個(gè)Undo頁面的鏈表,一個(gè)稱之為insert undo
鏈表,另一個(gè)稱之為update undo
鏈表,畫個(gè)示意圖就是這樣:
另外,InnoDB
對(duì)普通表和臨時(shí)表的記錄改動(dòng)時(shí)產(chǎn)生的undo
日志要分別記錄(后邊會(huì)有講解),所以在一個(gè)事務(wù)中最多有4個(gè)以Undo
頁面為節(jié)點(diǎn)組成的鏈表:
當(dāng)然,并不是在事務(wù)一開始就會(huì)為這個(gè)事務(wù)分配這4個(gè)鏈表,而是按需分配,具體分配策略如下:
- 剛剛開啟事務(wù)時(shí),一個(gè)Undo頁面鏈表也不分配。
- 當(dāng)事務(wù)執(zhí)行過程中向普通表中插入記錄或者執(zhí)行更新記錄主鍵- - 的操作之后,就會(huì)為其分配一個(gè)普通表的insert undo鏈表。
- 當(dāng)事務(wù)執(zhí)行過程中刪除或者更新了普通表中的記錄之后,就會(huì)為其分配一個(gè)普通表的update undo鏈表。
- 當(dāng)事務(wù)執(zhí)行過程中向臨時(shí)表中插入記錄或者執(zhí)行更新記錄主鍵的操作之后,就會(huì)為其分配一個(gè)臨時(shí)表的insert undo鏈表。
- 當(dāng)事務(wù)執(zhí)行過程中刪除或者更新了臨時(shí)表中的記錄之后,就會(huì)為其分配一個(gè)臨時(shí)表的update undo鏈表。
- 總結(jié)一下就是:什么時(shí)候需要啥時(shí)候再分配,不需要就不分配。
總結(jié)一下就是:什么時(shí)候需要啥時(shí)候再分配,不需要就不分配。
6.2 多個(gè)事務(wù)中的Undo頁面鏈表
為了盡可能提高undo日志的寫入效率,不同事務(wù)執(zhí)行過程中產(chǎn)生的undo日志需要被寫入到不同的Undo頁面鏈表中。比如說現(xiàn)在有事務(wù)id分別為
1、2的兩個(gè)事務(wù),我們分別稱之為trx 1
和trx 2
,假設(shè)在這兩個(gè)事務(wù)執(zhí)行過程中:
- trx 1對(duì)普通表做了DELETE操作,對(duì)臨時(shí)表做了INSERT和UPDATE操作。
- InnoDB會(huì)為trx 1分配3個(gè)鏈表,分別是:
- 針對(duì)普通表的update undo鏈表
- 針對(duì)臨時(shí)表的insert undo鏈表
- 針對(duì)臨時(shí)表的update undo鏈表。
- trx 2對(duì)普通表做了INSERT、UPDATE和DELETE操作,沒有對(duì)臨時(shí)表做改動(dòng)。
- InnoDB會(huì)為trx 2分配2個(gè)鏈表,分別是:
- 針對(duì)普通表的insert undo鏈表
- 針對(duì)普通表的update undo鏈表。
綜上所述,在trx 1
和trx 2
執(zhí)行過程中,InnoDB
共需為這兩個(gè)事務(wù)分配5個(gè)Undo
頁面鏈表,畫個(gè)圖就是這樣:
如果有更多的事務(wù),那就意味著可能會(huì)產(chǎn)生更多的Undo頁面鏈表。
七、undo日志具體寫入過程
7.1 段(Segment)的概念
如果你有認(rèn)真看過表空間那一章的話,對(duì)這個(gè)段的概念應(yīng)該印象深刻,我們當(dāng)時(shí)花了非常大的篇幅來嘮叨這個(gè)概念。簡單講,這個(gè)段是一個(gè)邏輯上的概念,本質(zhì)上是由若干個(gè)零散頁面和若干個(gè)完整的區(qū)組成的。比如一個(gè)B+樹索引被劃分成兩個(gè)段,一個(gè)葉子節(jié)點(diǎn)段,一個(gè)非葉子節(jié)點(diǎn)段,這樣葉子節(jié)點(diǎn)就可以被盡可能的存到一起,非葉子節(jié)點(diǎn)被盡可能的存到一起。每一個(gè)段對(duì)應(yīng)一個(gè)INODE Entry結(jié)構(gòu),這個(gè)INODE Entry結(jié)構(gòu)描述了這個(gè)段的各種信息,比如段的ID,段內(nèi)的各種鏈表基節(jié)點(diǎn),零散頁面的頁號(hào)有哪些等信息(具體該結(jié)構(gòu)中每個(gè)屬性的意思大家可以到表空間那一章里再次重溫一下)。我們前邊也說過,為了定位一個(gè)INODE Entry,InnoDB的設(shè)計(jì)了一個(gè)Segment Header
的結(jié)構(gòu):
整個(gè)Segment Header
占用10個(gè)字節(jié)大小,各個(gè)屬性的意思如下:
- Space ID of the INODE Entry:INODE Entry結(jié)構(gòu)所在的表空間ID。
- Page Number of the INODE Entry:INODE Entry結(jié)構(gòu)所在的頁面頁號(hào)。
- Byte Offset of the INODE Ent:INODE Entry結(jié)構(gòu)在該頁面中的偏移量
知道了表空間ID、頁號(hào)、頁內(nèi)偏移量,不就可以唯一定位一個(gè)INODE Entry的地址了么~
小提士:
這部分關(guān)于段的各種概念我們?cè)诒砜臻g那一章中都有詳細(xì)解釋,在這里重提一下只是為了喚醒大家沉睡的記憶,如果有任何不清楚的地方可以再次跳回表空間的那一章仔細(xì)讀一下
7.2 Undo Log Segment Header
InnoDB
的規(guī)定,每一個(gè)Undo頁面鏈表都對(duì)應(yīng)著一個(gè)段,稱之為Undo Log Segment
。也就是說鏈表中的頁面都是從這個(gè)段里邊申請(qǐng)的,所以他們?cè)赨ndo頁面鏈表的第一個(gè)頁面,也就是上邊提到的first undo page
中設(shè)計(jì)了一個(gè)稱之為Undo Log Segment Header
的部分,這個(gè)部分中包含了該鏈表對(duì)應(yīng)的段的segment header
信息以及其他的一些關(guān)于這個(gè)段的信息,所以Undo頁
面鏈表的第一個(gè)頁面其實(shí)長這樣:
可以看到這個(gè)Undo鏈表的第一個(gè)頁面比普通頁面多了個(gè)Undo Log Segment Header
,我們來看一下它的結(jié)構(gòu):
其中各個(gè)屬性的意思如下:
TRX_UNDO_STATE
:本Undo頁面鏈表處在什么狀態(tài)。一個(gè)Undo Log Segment
可能處在的狀態(tài)包括如下:TRX_UNDO_ACTIVE
:活躍狀態(tài),也就是一個(gè)活躍的事務(wù)正在往這個(gè)段里邊寫入undo日志。TRX_UNDO_CACHED
:被緩存的狀態(tài)。處在該狀態(tài)的Undo頁面鏈表等待著之后被其他事務(wù)重用。TRX_UNDO_TO_FREE
:對(duì)于insert undo鏈表來說,如果在它對(duì)應(yīng)的事務(wù)提交之后,該鏈表不能被重用,那么就會(huì)處于這種狀態(tài)。TRX_UNDO_TO_PURGE
:對(duì)于update undo鏈表來說,如果在它對(duì)應(yīng)的事務(wù)提交之后,該鏈表不能被重用,那么就會(huì)處于這種狀態(tài)。TRX_UNDO_PREPARED
:包含處于PREPARE階段的事務(wù)產(chǎn)生的undo日志
小提士:
Undo頁面鏈表什么時(shí)候會(huì)被重用,怎么重用我們之后會(huì)詳細(xì)說的。事務(wù)的PREPARE階段是在所謂的分布式事務(wù)中才出現(xiàn)的,本書中不會(huì)介紹更多關(guān)于分布式事務(wù)的事情,所以大家目前忽略這個(gè)狀態(tài)就好了
TRX_UNDO_LAST_LOG
:本Undo頁面鏈表中最后一個(gè)Undo Log Header
的位置。TRX_UNDO_FSEG_HEADER
:本Undo
頁面鏈表對(duì)應(yīng)的段的Segment Header
信息(就是我們上一節(jié)介紹的那個(gè)10字節(jié)結(jié)構(gòu),通過這個(gè)信息可以找到該段對(duì)應(yīng)的INODE Entry
)TRX_UNDO_PAGE_LIST
:Undo頁面鏈表的基節(jié)點(diǎn)。
我們上邊說Undo頁面的Undo Page Header部分有一個(gè)12字節(jié)大小的TRX_UNDO_PAGE_NODE
屬性,這個(gè)屬性代表一個(gè)List Node
結(jié)構(gòu)。每一個(gè)Undo
頁面都包含Undo Page Header
結(jié)構(gòu),這些頁面就可以通過這個(gè)屬性連成一個(gè)鏈表。這個(gè)TRX_UNDO_PAGE_LIST
屬性代表著這個(gè)鏈表的基節(jié)點(diǎn),當(dāng)然這個(gè)基節(jié)點(diǎn)只存在于Undo
頁面鏈表的第一個(gè)頁面,也就是first undo page
中。
Undo Log Header
一個(gè)事務(wù)在向Undo
頁面中寫入undo
日志時(shí)的方式是十分簡單暴力的,就是直接往里寫,寫完一條緊接著寫另一條,各條undo
日志之間是親密無間的。寫完一個(gè)Undo頁面后,再從段里申請(qǐng)一個(gè)新頁面,然后把這個(gè)頁面插入到Undo頁面鏈表中,繼續(xù)往這個(gè)新申請(qǐng)的頁面中寫。InnoDB的認(rèn)為同一個(gè)事務(wù)向一個(gè)Undo頁面鏈表中寫入的undo日志算是一個(gè)組,比方說我們上邊介紹的trx 1由于會(huì)分配3個(gè)Undo頁面鏈表,也就會(huì)寫入3個(gè)組的undo日志;trx 2由于會(huì)分配2個(gè)Undo頁面鏈表,也就會(huì)寫入2個(gè)組的undo日志。在每寫入一組undo日志時(shí),都會(huì)在這組undo日志前先記錄一下關(guān)于這個(gè)組的一些屬性,InnoDB把存儲(chǔ)這些屬性的地方稱之為Undo Log Header
。所以Undo頁面鏈表的第一個(gè)頁面在真正寫入undo日志前,其實(shí)都會(huì)被填充Undo Page Header
、Undo Log Segment Header
、Undo Log Header
這3個(gè)部分,如圖所示:
這個(gè)Undo Log Header
具體的結(jié)構(gòu)如下:
又是一大堆屬性,我們先大致看一下它們都是啥意思:
TRX_UNDO_TRX_ID
:生成本組undo日志的事務(wù)idTRX_UNDO_TRX_NO
:事務(wù)提交后生成的一個(gè)需要序號(hào),使用此序號(hào)來標(biāo)記事務(wù)的提交順序(先提交的此序號(hào)小,后提交的此序號(hào)大)。TRX_UNDO_DEL_MARKS
:標(biāo)記本組undo
日志中是否包含由于Delete mark
操作產(chǎn)生的undo日志。TRX_UNDO_LOG_START
:表示本組undo日志中第一條undo日志的在頁面中的偏移量。TRX_UNDO_XID_EXISTS
:本組undo日志是否包含XID信息。TRX_UNDO_DICT_TRANS
:標(biāo)記本組undo日志是不是由DDL語句產(chǎn)生的。TRX_UNDO_TABLE_ID
:如果TRX_UNDO_DICT_TRANS為真,那么本屬性表示DDL語句操作的表的table id。TRX_UNDO_NEXT_LOG
:下一組的undo日志在頁面中開始的偏移量。TRX_UNDO_PREV_LOG
:上一組的undo日志在頁面中開始的偏移量。
小提士:
一般來說一個(gè)Undo頁面鏈表只存儲(chǔ)一個(gè)事務(wù)執(zhí)行過程中產(chǎn)生的一組undo日志,但是在某些情況下,可能會(huì)在一個(gè)事務(wù)提交之后,之后開啟的事務(wù)重復(fù)利用這個(gè)Undo頁面鏈表,這樣就會(huì)導(dǎo)致一個(gè)Undo頁面中可能存放多組Undo日志,TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG就是用來標(biāo)記下一組和上一組undo日志在頁面中的偏移量的。關(guān)于什么時(shí)候重用Undo頁面鏈表,怎么重用這個(gè)鏈表我們稍后會(huì)詳細(xì)說明的,現(xiàn)在先理解TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG這兩個(gè)屬性的意思就好了。
TRX_UNDO_HISTORY_NODE
:一個(gè)12字節(jié)的List Node結(jié)構(gòu),代表一個(gè)稱之為History鏈表的節(jié)點(diǎn)。
小結(jié)
對(duì)于沒有被重用的Undo
頁面鏈表來說,鏈表的第一個(gè)頁面,也就是first undo page
在真正寫入undo
日志前,會(huì)填充Undo Page Header、Undo Log Segment Header、Undo Log Header這3個(gè)部分
,之后才開始正式寫入undo日志。對(duì)于其他的頁面來說,也就是normal undo page
在真正寫入undo
日志前,只會(huì)填充Undo Page Header
。鏈表的List Base Node
存放到first undo page的Undo Log Segment Header部分
,List Node
信息存放到每一個(gè)Undo
頁面的undo Page Header
部分,所以畫一個(gè)Undo
頁面鏈表的示意圖就是這樣:
八、重用Undo頁面
我們前邊說為了能提高并發(fā)執(zhí)行的多個(gè)事務(wù)寫入undo
日志的性能,InnoDB
決定為每個(gè)事務(wù)單獨(dú)分配相應(yīng)的Undo
頁面鏈表(最多可能單獨(dú)分配4個(gè)鏈表)。但是這樣也造成了一些問題,比如其實(shí)大部分事務(wù)執(zhí)行過程中可能只修改了一條或幾條記錄,針對(duì)某個(gè)Undo頁面鏈表只產(chǎn)生了非常少的undo日志,這些undo日志可能只占用一丟丟存儲(chǔ)空間,每開啟一個(gè)事務(wù)就新創(chuàng)建一個(gè)Undo
頁面鏈表(雖然這個(gè)鏈表中只有一個(gè)頁面)來存儲(chǔ)這么一丟丟undo日志豈不是太浪費(fèi)了么?的確是挺浪費(fèi),于是InnoDB決定在事務(wù)提交后在某些情況下重用該事務(wù)的Undo
頁面鏈表。一個(gè)Undo
頁面鏈表是否可以被重用的條件很簡單:
該鏈表中只包含一個(gè)Undo
頁面
如果一個(gè)事務(wù)執(zhí)行過程中產(chǎn)生了非常多的undo日志,那么它可能申請(qǐng)非常多的頁面加入到Undo頁面鏈表中。在該事物提交后,如果將整個(gè)鏈表中的頁面都重用,那就意味著即使新的事務(wù)并沒有向該Undo
頁面鏈表中寫入很多undo
日志,那該鏈表中也得維護(hù)非常多的頁面,那些用不到的頁面也不能被別的事務(wù)所使用,這樣就造成了另一種浪費(fèi)。所以InnoDB
的規(guī)定,只有在Undo頁面鏈表中只包含一個(gè)Undo
頁面時(shí),該鏈表才可以被下一個(gè)事務(wù)所重用
該Undo頁面已經(jīng)使用的空間小于整個(gè)頁面空間的3/4
我們前邊說過,Undo頁面鏈表按照存儲(chǔ)的undo日志所屬的大類可以被分為insert undo
鏈表和update undo
鏈表兩種,這兩種鏈表在被重用時(shí)的策略也是不同的,我們分別看一下
- insert undo鏈表
insert undo
鏈表中只存儲(chǔ)類型為TRX_UNDO_INSERT_REC
的undo
日志,這種類型的undo日志在事務(wù)提交之后就沒用了,就可以被清除掉。所以在某個(gè)事務(wù)提交后,重用這個(gè)事務(wù)的insert undo鏈表(這個(gè)鏈表中只有一個(gè)頁面)時(shí),可以直接把之前事務(wù)寫入的一組undo日志覆蓋掉,從頭開始寫入新事務(wù)的一組undo日志,如下圖所示:
如圖所示,假設(shè)有一個(gè)事務(wù)使用的insert undo
鏈表,到事務(wù)提交時(shí),只向insert undo鏈表中插入了3條undo日志,這個(gè)insert undo鏈表只申請(qǐng)了一個(gè)Undo頁面。假設(shè)此刻該頁面已使用的空間小于整個(gè)頁面大小的3/4,那么下一個(gè)事務(wù)就可以重用這個(gè)insert undo
鏈表(鏈表中只有一個(gè)頁面)。假設(shè)此時(shí)有一個(gè)新事務(wù)重用了該insert undo
鏈表,那么可以直接把舊的一組undo日志覆蓋掉,寫入一組新的undo
日志。
- update undo鏈表
在一個(gè)事務(wù)提交后,它的update undo
鏈表中的undo
日志也不能立即刪除掉(這些日志用于MVCC,我們后邊會(huì)說的)。所以如果之后的事務(wù)想重用update undo
鏈表時(shí),就不能覆蓋之前事務(wù)寫入的undo
日志。這樣就相當(dāng)于在同一個(gè)Undo
頁面中寫入了多組的undo
日志,效果看起來就是這樣
九、回滾段
9.1 回滾段的概念
我們現(xiàn)在知道一個(gè)事務(wù)在執(zhí)行過程中最多可以分配4個(gè)Undo
頁面鏈表,在同一時(shí)刻不同事務(wù)擁有的Undo
頁面鏈表是不一樣的,所以在同一時(shí)刻系統(tǒng)里其實(shí)可以有許許多多個(gè)Undo頁面鏈表存在。為了更好的管理這些鏈表,InnoDB
又設(shè)計(jì)了一個(gè)稱之為Rollback Segment Header
的頁面,在這個(gè)頁面中存放了各個(gè)Undo
頁面鏈表的frist undo page
的頁號(hào),他們把這些頁號(hào)稱之為undo slot
。我們可以這樣理解,每個(gè)Undo
頁面鏈表都相當(dāng)于是一個(gè)班,這個(gè)鏈表的first undo page
就相當(dāng)于這個(gè)班的班長,找到了這個(gè)班的班長,就可以找到班里的其他同學(xué)(其他同學(xué)相當(dāng)于normal undo page
)。有時(shí)候?qū)W校需要向這些班級(jí)傳達(dá)一下精神,就需要把班長都召集在會(huì)議室,這個(gè)Rollback Segment Header
就相當(dāng)于是一個(gè)會(huì)議室。
我們看一下這個(gè)稱之為Rollback Segment Header
的頁面長啥樣(以默認(rèn)的16KB為例):
InnoDB
規(guī)定,每一個(gè)Rollback Segment Header
頁面都對(duì)應(yīng)著一個(gè)段,這個(gè)段就稱為Rollback Segment
,也就是回滾段
。與我們之前介紹的各種段不同的是,這個(gè)Rollback Segment
里其實(shí)只有一個(gè)頁面(這可能是InnoDB
可能覺得為了某個(gè)目的去分配頁面的話都得先申請(qǐng)一個(gè)段,或者他們覺得雖然目前版本的MySQL
里Rollback Segment
里其實(shí)只有一個(gè)頁面,但可能之后的版本里會(huì)增加頁面也說不定)。
了解了Rollback Segment
的含義之后,我們?cè)賮砜纯催@個(gè)稱之為Rollback Segment Header
的頁面的各個(gè)部分的含義都是啥意思:
TRX_RSEG_MAX_SIZE
:本Rollback Segment
中管理的所有Undo
頁面鏈表中的Undo
頁面數(shù)量之和的最大值。換句話說,本Rollback Segment中所有Undo頁面鏈表中的Undo頁面數(shù)量之和不能超過TRX_RSEG_MAX_SIZE代表的值
。
該屬性的值默認(rèn)為無限大,也就是我們想寫多少Undo頁面都可以。
小提士:
無限大其實(shí)也只是個(gè)夸張的說法,4個(gè)字節(jié)能表示最大的數(shù)也就是0xFFFFFFFF,但是我們之后會(huì)看到,0xFFFFFFFF這個(gè)數(shù)有特殊用途,所以實(shí)際上TRX_RSEG_MAX_SIZE的值為0xFFFFFFFE。
TRX_RSEG_HISTORY_SIZE
:History
鏈表占用的頁面數(shù)量。TRX_RSEG_HISTORY
:History
鏈表的基節(jié)點(diǎn)。TRX_RSEG_FSEG_HEADER
:本Rollback Segment
對(duì)應(yīng)的10字節(jié)大小的Segment Header
結(jié)構(gòu),通過它可以找到本段對(duì)應(yīng)的INODE Entry
。
TRX_RSEG_UNDO_SLOTS
:各個(gè)Undo
頁面鏈表的first undo page
的頁號(hào)集合,也就是undo slot
集合。
一個(gè)頁號(hào)占用4
個(gè)字節(jié),對(duì)于16KB
大小的頁面來說,這個(gè)TRX_RSEG_UNDO_SLOTS
部分共存儲(chǔ)了1024
個(gè)undo slot
,所以共需1024 × 4 = 4096個(gè)字節(jié)
9.2 從回滾段中申請(qǐng)Undo頁面鏈表
初始情況下,由于未向任何事務(wù)分配任何Undo
頁面鏈表,所以對(duì)于一個(gè)Rollback Segment Header
頁面來說,它的各個(gè)undo slot
都被設(shè)置成了一個(gè)特殊的值:FIL_NULL
(對(duì)應(yīng)的十六進(jìn)制就是0xFFFFFFFF),表示該undo slot
不指向任何頁面。
隨著時(shí)間的流逝,開始有事務(wù)需要分配Undo
頁面鏈表了,就從回滾段的第一個(gè)undo slot
開始,看看該undo slot
的值是不是FIL_NULL
:
- 如果是
FIL_NULL
,那么在表空間中新創(chuàng)建一個(gè)段(也就是Undo Log Segment
),然后從段里申請(qǐng)一個(gè)頁面作為Undo
頁面鏈表的first undo page
,然后把該undo slot
的值設(shè)置為剛剛申請(qǐng)的這個(gè)頁面的頁號(hào),這樣也就意味著這個(gè)undo slot
被分配給了這個(gè)事務(wù)。 - 如果不是
FIL_NULL
,說明該undo slot
已經(jīng)指向了一個(gè)undo
鏈表,也就是說這個(gè)undo slot
已經(jīng)被別的事務(wù)占用了,那就跳到下一個(gè)undo slot
,判斷該undo slot
的值是不是FIL_NULL
,重復(fù)上邊的步驟。
一個(gè)Rollback Segment Header
頁面中包含1024個(gè)undo slot
,如果這1024
個(gè)undo slot
的值都不為FIL_NULL
,這就意味著這1024
個(gè)undo slot
都已經(jīng)名花有主(被分配給了某個(gè)事務(wù)),此時(shí)由于新事務(wù)無法再獲得新的Undo
頁面鏈表,就會(huì)回滾這個(gè)事務(wù)并且給用戶報(bào)錯(cuò):
Too many active concurrent transactions
用戶看到這個(gè)錯(cuò)誤,可以選擇重新執(zhí)行這個(gè)事務(wù)(可能重新執(zhí)行時(shí)有別的事務(wù)提交了,該事務(wù)就可以被分配Undo
頁面鏈表了)。
當(dāng)一個(gè)事務(wù)提交時(shí),它所占用的undo slot
有兩種命運(yùn):
- 如果該
undo slot
指向的Undo
頁面鏈表符合被重用的條件(就是我們上邊說的Undo頁面鏈表只占用一個(gè)頁面并且已使用空間小于整個(gè)頁面的3/4)。
該undo slot
就處于被緩存的狀態(tài),InnoDB
規(guī)定這時(shí)該Undo
頁面鏈表的TRX_UNDO_STATE
屬性(該屬性在first undo page
的Undo Log Segment Heade
r部分)會(huì)被設(shè)置為TRX_UNDO_CACHED
。
被緩存的undo slot
都會(huì)被加入到一個(gè)鏈表,根據(jù)對(duì)應(yīng)的Undo
頁面鏈表的類型不同,也會(huì)被加入到不同的鏈表:
- 如果對(duì)應(yīng)的
Undo
頁面鏈表是insert undo
鏈表,則該undo slot
會(huì)被加入insert undo cached
鏈表。 - 如果對(duì)應(yīng)的
Undo
頁面鏈表是update undo
鏈表,則該undo slot
會(huì)被加入update undo cached
鏈表。
一個(gè)回滾段就對(duì)應(yīng)著上述兩個(gè)cached
鏈表,如果有新事務(wù)要分配undo slot
時(shí),先從對(duì)應(yīng)的cached
鏈表中找。如果沒有被緩存的undo slot
,才會(huì)到回滾段的Rollback Segment Header
頁面中再去找。
- 如果該
undo slot
指向的Undo
頁面鏈表不符合被重用的條件,那么針對(duì)該undo slot
對(duì)應(yīng)的Undo
頁面鏈表類型不同,也會(huì)有不同的處理: - 如果對(duì)應(yīng)的
Undo
頁面鏈表是insert undo
鏈表,則該Undo
頁面鏈表的TRX_UNDO_STATE
屬性會(huì)被設(shè)置為TRX_UNDO_TO_FREE
,之后該Undo
頁面鏈表對(duì)應(yīng)的段會(huì)被釋放掉(也就意味著段中的頁面可以被挪作他用),然后把該undo slot
的值設(shè)置為FIL_NULL
。 - 如果對(duì)應(yīng)的
Undo
頁面鏈表是update undo
鏈表,則該Undo
頁面鏈表的TRX_UNDO_STATE
屬性會(huì)被設(shè)置為TRX_UNDO_TO_PRUGE
,則會(huì)將該undo slot
的值設(shè)置為FIL_NULL
,然后將本次事務(wù)寫入的一組undo
日志放到所謂的History
鏈表中(需要注意的是,這里并不會(huì)將Undo頁面鏈表對(duì)應(yīng)的段給釋放掉,因?yàn)檫@些undo
日志還有用呢~)
9.3 多個(gè)回滾段
我們說一個(gè)事務(wù)執(zhí)行過程中最多分配4個(gè)Undo頁面鏈表
,而一個(gè)回滾段里只有1024個(gè)undo slot
,很顯然undo slot
的數(shù)量有點(diǎn)少啊。我們即使假設(shè)一個(gè)讀寫事務(wù)執(zhí)行過程中只分配1
個(gè)Undo
頁面鏈表,那1024
個(gè)undo slot
也只能支持1024
個(gè)讀寫事務(wù)同時(shí)執(zhí)行,再多了就崩潰了。這就相當(dāng)于會(huì)議室只能容下1024
個(gè)班長同時(shí)開會(huì),如果有幾千人同時(shí)到會(huì)議室開會(huì)的話,那后來的那些班長就沒地方坐了,只能等待前邊的人開完會(huì)自己再進(jìn)去開。
話說在InnoDB
的早期發(fā)展階段的確只有一個(gè)回滾段,但是InnoDB
后來意識(shí)到了這個(gè)問題,咋解決這問題呢?會(huì)議室不夠,多蓋幾個(gè)會(huì)議室不就得了。所以InnoDB
一口氣定義了128
個(gè)回滾段,也就相當(dāng)于有了128 × 1024 = 131072個(gè)undo slot
。假設(shè)一個(gè)讀寫事務(wù)執(zhí)行過程中只分配1
個(gè)Undo
頁面鏈表,那么就可以同時(shí)支持131072
個(gè)讀寫事務(wù)并發(fā)執(zhí)行(這么多事務(wù)在一臺(tái)機(jī)器上并發(fā)執(zhí)行,還真沒見過呢~)
每個(gè)回滾段都對(duì)應(yīng)著一個(gè)Rollback Segment Header
頁面,有128
個(gè)回滾段,自然就要有128
個(gè)Rollback Segment Header
頁面,這些頁面的地址總得找個(gè)地方存一下吧!于是InnoDB
在系統(tǒng)表空間的第5
號(hào)頁面的某個(gè)區(qū)域包含了128個(gè)8字節(jié)大小的格子:
每個(gè)8字節(jié)的格子的構(gòu)造就像這樣:
如果所示,每個(gè)8字節(jié)的格子其實(shí)由兩部分組成:
- 4字節(jié)大小的
Space ID
,代表一個(gè)表空間的ID。 - 4字節(jié)大小的
Page number
,代表一個(gè)頁號(hào)。
也就是說每個(gè)8字節(jié)大小的格子
相當(dāng)于一個(gè)指針,指向某個(gè)表空間中的某個(gè)頁面,這些頁面就是Rollback Segment Header
。這里需要注意的一點(diǎn)事,要定位一個(gè)Rollback Segment Header還需要知道對(duì)應(yīng)的表空間ID,這也就意味著不同的回滾段可能分布在不同的表空間中。
所以通過上邊的敘述我們可以大致清楚,在系統(tǒng)表空間的第5
號(hào)頁面中存儲(chǔ)了128
個(gè)Rollback Segment Header
頁面地址,每個(gè)Rollback Segment Header
就相當(dāng)于一個(gè)回滾段。在Rollback Segment Header
頁面中,又包含1024個(gè)undo slot
,每個(gè)undo slot
都對(duì)應(yīng)一個(gè)Undo
頁面鏈表。我們畫個(gè)示意圖:
把圖一畫出來就清爽多了。
9.4 回滾段的分類
我們把這128個(gè)回滾段給編一下號(hào),最開始的回滾段稱之為第0號(hào)回滾段,之后依次遞增,最后一個(gè)回滾段就稱之為第127號(hào)回滾段。這128個(gè)回滾段可以被分成兩大類:
第0號(hào)、第33~127號(hào)回滾段屬于一類
。其中第0號(hào)回滾段必須在系統(tǒng)表空間中(就是說第0號(hào)回滾段對(duì)應(yīng)的Rollback Segment Header頁面必須在系統(tǒng)表空間中),第33~127號(hào)回滾段既可以在系統(tǒng)表空間中,也可以在自己配置的undo表空間中,關(guān)于怎么配置我們稍后再說。
如果一個(gè)事務(wù)在執(zhí)行過程中由于對(duì)普通表的記錄做了改動(dòng)需要分配Undo頁面鏈表時(shí),必須從這一類的段中分配相應(yīng)的undo slot。
第1~32號(hào)回滾段屬于一類
。這些回滾段必須在臨時(shí)表空間(對(duì)應(yīng)著數(shù)據(jù)目錄中的ibtmp1文件)中。
如果一個(gè)事務(wù)在執(zhí)行過程中由于對(duì)臨時(shí)表的記錄做了改動(dòng)需要分配Undo頁面鏈表時(shí),必須從這一類的段中分配相應(yīng)的undo slot
。
也就是說如果一個(gè)事務(wù)在執(zhí)行過程中既對(duì)普通表的記錄做了改動(dòng),又對(duì)臨時(shí)表的記錄做了改動(dòng),那么需要為這個(gè)記錄分配2個(gè)回滾段,再分別到這兩個(gè)回滾段中分配對(duì)應(yīng)的undo slot
。
不知道大家有沒有疑惑,為啥要把針對(duì)普通表和臨時(shí)表來劃分不同種類的回滾段呢?這個(gè)還得從Undo
頁面本身說起,我們說Undo
頁面其實(shí)是類型為FIL_PAGE_UNDO_LOG
的頁面的簡稱,說到底它也是一個(gè)普通的頁面。我們前邊說過,在修改頁面之前一定要先把對(duì)應(yīng)的redo
日志寫上,這樣在系統(tǒng)奔潰重啟時(shí)才能恢復(fù)到奔潰前的狀態(tài)。我們向Undo
頁面寫入undo
日志本身也是一個(gè)寫頁面的過程,InnoDB
為此還設(shè)計(jì)了許多種redo
日志的類型,比方說MLOG_UNDO_HDR_CREATE
、MLOG_UNDO_INSERT
、MLOG_UNDO_INIT
等等,也就是說我們對(duì)Undo
頁面做的任何改動(dòng)都會(huì)記錄相應(yīng)類型的redo
日志。但是對(duì)于臨時(shí)表來說,因?yàn)樾薷呐R時(shí)表而產(chǎn)生的undo
日志只需要在系統(tǒng)運(yùn)行過程中有效,如果系統(tǒng)奔潰了,那么在重啟時(shí)也不需要恢復(fù)這些undo
日志所在的頁面,所以在寫針對(duì)臨時(shí)表的Undo
頁面時(shí),并不需要記錄相應(yīng)的redo
日志??偨Y(jié)一下針對(duì)普通表和臨時(shí)表劃分不同種類的回滾段的原因:在修改針對(duì)普通表的回滾段中的Undo
頁面時(shí),需要記錄對(duì)應(yīng)的redo
日志,而修改針對(duì)臨時(shí)表的回滾段中的Undo
頁面時(shí),不需要記錄對(duì)應(yīng)的redo
日志。
小提士:
如果我們僅僅對(duì)普通表的記錄做了改動(dòng),那么只會(huì)為該事務(wù)分配針對(duì)普通表的回滾段,不分配針對(duì)臨時(shí)表的回滾段。但是如果我們僅僅對(duì)臨時(shí)表的記錄做了改動(dòng),那么既會(huì)為該事務(wù)分配針對(duì)普通表的回滾段,又會(huì)為其分配針對(duì)臨時(shí)表的回滾段(不過分配了回滾段并不會(huì)立即分配undo slot,只有在真正需要Undo頁面鏈表時(shí)才會(huì)去分配回滾段中的undo slot)。
9.5 為事務(wù)分配Undo頁面鏈表詳細(xì)過程
上邊說了一大堆的概念,大家應(yīng)該有一點(diǎn)點(diǎn)的小暈,接下來我們以事務(wù)對(duì)普通表的記錄做改動(dòng)為例,給大家梳理一下事務(wù)執(zhí)行過程中分配Undo頁面
鏈表時(shí)的完整過程,
- 事務(wù)在執(zhí)行過程中對(duì)普通表的記錄首次做改動(dòng)之前,首先會(huì)到系統(tǒng)表空間的第5號(hào)頁面中分配一個(gè)回滾段(其實(shí)就是獲取一個(gè)
Rollback Segment Header
頁面的地址)。一旦某個(gè)回滾段被分配給了這個(gè)事務(wù),那么之后該事務(wù)中再對(duì)普通表的記錄做改動(dòng)時(shí),就不會(huì)重復(fù)分配了。
使用傳說中的round-robin
(循環(huán)使用)方式來分配回滾段。比如當(dāng)前事務(wù)分配了第0號(hào)回滾段,那么下一個(gè)事務(wù)就要分配第33號(hào)回滾段,下下個(gè)事務(wù)就要分配第34號(hào)回滾段,簡單一點(diǎn)的說就是這些回滾段被輪著分配給不同的事務(wù)(就是這么簡單粗暴,沒啥好說的)。
- 在分配到回滾段后,首先看一下這個(gè)回滾段的兩個(gè)
cached
鏈表有沒有已經(jīng)緩存了的undo slot
,比如如果事務(wù)做的是INSERT
操作,就去回滾段對(duì)應(yīng)的insert undo cached
鏈表中看看有沒有緩存的undo slot
;如果事務(wù)做的是DELETE
操作,就去回滾段對(duì)應(yīng)的update undo cached
鏈表中看看有沒有緩存的undo slot
。如果有緩存的undo slot
,那么就把這個(gè)緩存的undo slot
分配給該事務(wù)。 - 如果沒有緩存的
undo slot
可供分配,那么就要到Rollback Segment Header
頁面中找一個(gè)可用的undo slot
分配給當(dāng)前事務(wù)。
從Rollback Segment Header
頁面中分配可用的undo slot
的方式我們上邊也說過了,就是從第0個(gè)undo slot
開始,如果該undo slot
的值為FIL_NULL
,意味著這個(gè)undo slot
是空閑的,就把這個(gè)undo slot
分配給當(dāng)前事務(wù),否則查看第1個(gè)undo slot
是否滿足條件,依次類推,直到最后一個(gè)undo slot
。如果這1024個(gè)undo slot都沒有值為FIL_NULL
的情況,就直接報(bào)錯(cuò)嘍(一般不會(huì)出現(xiàn)這種情況)~
- 找到可用的
undo slot
后,如果該undo slot
是從cached
鏈表中獲取的,那么它對(duì)應(yīng)的Undo Log Segment
已經(jīng)分配了,否則的話需要重新分配一個(gè)Undo Log Segment
,然后從該Undo Log Segment
中申請(qǐng)一個(gè)頁面作為Undo
頁面鏈表的first undo page
。 - 然后事務(wù)就可以把
undo
日志寫入到上邊申請(qǐng)的Undo頁面鏈表了
!
對(duì)臨時(shí)表的記錄做改動(dòng)的步驟和上述的一樣,就不贅述了。不過需要再次強(qiáng)調(diào)一次,如果一個(gè)事務(wù)在執(zhí)行過程中既對(duì)普通表的記錄做了改動(dòng),又對(duì)臨時(shí)表的記錄做了改動(dòng),那么需要為這個(gè)記錄分配2個(gè)回滾段。并發(fā)執(zhí)行的不同事務(wù)其實(shí)也可以被分配相同的回滾段,只要分配不同的undo slot
就可以了。
9.6 回滾段相關(guān)配置
9.6.1 配置回滾段數(shù)量
我們前邊說系統(tǒng)中一共有128
個(gè)回滾段,其實(shí)這只是默認(rèn)值,我們可以通過啟動(dòng)參數(shù)innodb_rollback_segments
來配置回滾段的數(shù)量,可配置的范圍是1~128
。但是這個(gè)參數(shù)并不會(huì)影響針對(duì)臨時(shí)表的回滾段數(shù)量,針對(duì)臨時(shí)表的回滾段數(shù)量一直是32
,也就是說:
- 如果我們把
innodb_rollback_segments
的值設(shè)置為1
,那么只會(huì)有1
個(gè)針對(duì)普通表的可用回滾段,但是仍然有32
個(gè)針對(duì)臨時(shí)表的可用回滾段。 - 如果我們把
innodb_rollback_segments
的值設(shè)置為2~33
之間的數(shù),效果和將其設(shè)置為1
是一樣的。 - 如果我們把
innodb_rollback_segments
設(shè)置為大于33
的數(shù),那么針對(duì)普通表的可用回滾段數(shù)量就是該值減去32
。
9.6.2 配置undo表空間
默認(rèn)情況下,針對(duì)普通表設(shè)立的回滾段(第0
號(hào)以及第33~127
號(hào)回滾段)都是被分配到系統(tǒng)表空間的。其中的第0
號(hào)回滾段是一直在系統(tǒng)表空間的,但是第33~127
號(hào)回滾段可以通過配置放到自定義的undo
表空間中。但是這種配置只能在系統(tǒng)初始化(創(chuàng)建數(shù)據(jù)目錄時(shí))的時(shí)候使用,一旦初始化完成,之后就不能再次更改了。我們看一下相關(guān)啟動(dòng)參數(shù):
- 通過
innodb_undo_directory
指定undo
表空間所在的目錄,如果沒有指定該參數(shù),則默認(rèn)undo
表空間所在的目錄就是數(shù)據(jù)目錄。 - 通過
innodb_undo_tablespaces
定義undo
表空間的數(shù)量。該參數(shù)的默認(rèn)值為0
,表明不創(chuàng)建任何undo
表空間。
第33~127號(hào)回滾段可以平均分布到不同的undo表空間中。
小提士:
如果我們?cè)谙到y(tǒng)初始化的時(shí)候指定了創(chuàng)建了undo表空間,那么系統(tǒng)表空間中的第0號(hào)回滾段將處于不可用狀態(tài)。
比如我們?cè)谙到y(tǒng)初始化時(shí)指定的innodb_rollback_segments
為35
,innodb_undo_tablespaces
為2
,這樣就會(huì)將第33
、34
號(hào)回滾段分別分布到一個(gè)undo
表空間中。
設(shè)立 到此這篇關(guān)于一篇文章帶你了解MySQL之undo日志的文章就介紹到這了,更多相關(guān)MySQL undo日志內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!undo表空間
的一個(gè)好處就是在undo
表空間中的文件大到一定程度時(shí),可以自動(dòng)的將該undo表空間截?cái)?/code>(
truncate
)成一個(gè)小文件。而系統(tǒng)表空間的大小只能不斷的增大,卻不能截?cái)唷?/p>
總結(jié)
相關(guān)文章
Centos8安裝mysql8的詳細(xì)過程(免安裝版/或者二進(jìn)制包方式安裝)
這篇文章主要介紹了Centos8安裝mysql8的詳細(xì)過程(免安裝版/或者二進(jìn)制包方式安裝),使用二進(jìn)制包方式安裝首先檢查服務(wù)器上是否安裝有mysql然后開始安裝配置,本文分步驟給大家講解的非常詳細(xì),需要的朋友可以參考下2022-11-11MySQL中distinct和group?by去重效率區(qū)別淺析
distinct 與 group by均可用于去重,下面這篇文章主要給大家介紹了關(guān)于MySQL中distinct和group?by去重效率區(qū)別的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03MySQL外鍵類型及應(yīng)用場(chǎng)景總結(jié)
這篇文章主要介紹了?MySQL?外鍵的類型(RESTRICT、CASCADE、SET?NULL、NO?ACTION)及其應(yīng)用場(chǎng)景、優(yōu)缺點(diǎn)和使用注意事項(xiàng),通過創(chuàng)建和測(cè)試外鍵,闡述了不同類型外鍵在主表刪除或更新數(shù)據(jù)時(shí)子表的變化,需要的朋友可以參考下2024-12-12MySQL啟動(dòng)失敗之MySQL服務(wù)無法啟動(dòng)的原因及解決
這篇文章主要介紹了MySQL啟動(dòng)失敗之MySQL服務(wù)無法啟動(dòng)的原因及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12mysql增量備份及斷點(diǎn)恢復(fù)腳本實(shí)例
生產(chǎn)環(huán)境中在mysql中誤操作是非常正常的,所以就需要用到mysql的增量備份恢復(fù)。增量備份是我們經(jīng)常用到的,它可以指定某個(gè)誤操作的時(shí)間以及位置點(diǎn)進(jìn)行數(shù)據(jù)恢復(fù),更加準(zhǔn)確的恢復(fù)我們想要還原的數(shù)據(jù)。2018-09-09mysql8.0.18下安裝winx64的詳細(xì)教程(圖文詳解)
這篇文章主要介紹了安裝mysql-8.0.18-win-x64的詳細(xì)教程,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11