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