Mysql-5-之InnoDB數(shù)據(jù)頁結(jié)構(gòu)

不同類型的頁簡介

前邊我們簡單提了一下頁的概念,它是InnoDB管理存儲空間的基本單位,一個(gè)頁的大小一般是16KB。InnoDB為了不同的目的而設(shè)計(jì)了許多種不同類型的頁,比如存放表空間頭部信息的頁,存放Insert Buffer信息的頁,存放INODE信息的頁,存放undo日志信息的頁等等等等。

我們聚焦的是那些存放我們表中記錄的那種類型的頁,官方稱這種存放記錄的頁為索引(INDEX)頁,鑒于我們還沒有了解過索引是個(gè)什么東西,而這些表中的記錄就是我們?nèi)粘?谥兴Q的數(shù) 據(jù),所以目前還是叫這種存放記錄的頁為數(shù)據(jù)頁吧。

數(shù)據(jù)頁結(jié)構(gòu)的快速瀏覽

數(shù)據(jù)頁代表的這塊16KB大小的存儲空間可以被劃分為多個(gè)部分,不同部分有不同的功能,各個(gè)部分如圖所示:


image.png

從圖中可以看出,一個(gè)InnoDB數(shù)據(jù)頁的存儲空間大致被劃分成了7個(gè)部分,有的部分占用的字節(jié)數(shù)是確定的,有的部分占用的字節(jié)數(shù)是不確定的。下邊我們用表格的方式來大致描述一下這7個(gè)部分都存儲 一些啥內(nèi)容(快速的瞅一眼就行了,后邊會詳細(xì)嘮叨的):

名稱 中文名 占用空間大小 簡單描述
File Header 文件頭部 38字節(jié) 頁的一些通用信息
Page Header 頁面頭部 56字節(jié) 數(shù)據(jù)頁專有的一些信息
Infimum + Supremum 最小記錄和最大記錄 26字節(jié) 兩個(gè)虛擬的行記錄
User Records 用戶記錄 不確定 實(shí)際存儲的行記錄內(nèi)容
Free Space 空閑空間 不確定 頁中尚未使用的空間
Page Directory 頁面目錄 不確定 頁中的某些記錄的相對位置
File Trailer 文件尾部 8字節(jié) 校驗(yàn)頁是否完整

記錄在頁中的存儲

在頁的7個(gè)組成部分中,我們自己存儲的記錄會按照我們指定的行格式存儲到User Records部分。但是在一開始生成頁的時(shí)候,其實(shí)并沒有User Records這個(gè)部分,每當(dāng)我們插入一條記錄,都會從Free Space部分,也就是尚未使用的存儲空間中申請一個(gè)記錄大小的空間劃分到User Records部分,當(dāng)Free Space部分的空間全部被User Records部分替代掉之后,也就意味著這個(gè)頁使用完了,如果還有新的 記錄插入的話,就需要去申請新的頁了,這個(gè)過程的圖示如下:


image.png

為了更好的管理在User Records中的這些記錄,InnoDB可費(fèi)了一番力氣呢,在哪費(fèi)力氣了呢?不就是把記錄按照指定的行格式一條一條擺在User Records部分么?其實(shí)這話還得從記錄行格式的記錄頭信 息中說起。

記錄頭信息的秘密

為了故事的順利發(fā)展,我們先創(chuàng)建一個(gè)表:

mysql> CREATE TABLE page_demo(
    ->     c1 INT,
    ->     c2 INT,
    ->     c3 VARCHAR(10000),
    ->     PRIMARY KEY (c1)
    -> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.03 sec)

這個(gè)新創(chuàng)建的page_demo表有3個(gè)列,其中c1和c2列是用來存儲整數(shù)的,c3列是用來存儲字符串的。需要注意的是,我們把 c1 列指定為主鍵,所以在具體的行格式中InnoDB就沒必要為我們?nèi)?chuàng)建那個(gè)所謂 的 row_id 隱藏列了。而且我們?yōu)檫@個(gè)表指定了ascii字符集以及Compact的行格式。所以這個(gè)表中記錄的行格式示意圖就是這樣的:


image.png

從圖中可以看到,我們特意把記錄頭信息的5個(gè)字節(jié)的數(shù)據(jù)給標(biāo)出來了,說明它很重要,我們再次先把這些記錄頭信息中各個(gè)屬性的大體意思瀏覽一下(我們目前使用Compact行格式進(jìn)行演示):

名稱 大小(單位:bit) 描述
預(yù)留位1 1 沒有使用
預(yù)留位2 1 沒有使用
delete_mask 1 標(biāo)記該記錄是否被刪除
min_rec_mask 1 B+樹的每層非葉子節(jié)點(diǎn)中的最小記錄都會添加該標(biāo)記
n_owned 4 表示當(dāng)前記錄擁有的記錄數(shù)
heap_no 13 表示當(dāng)前記錄在記錄堆的位置信息
record_type 3 表示當(dāng)前記錄的類型,0表示普通記錄,1表示B+樹非葉節(jié)點(diǎn)記錄,2表示最小記錄,3表示最大記錄
next_record 16 表示下一條記錄的相對位置

由于我們現(xiàn)在主要在嘮叨記錄頭信息的作用,所以為了大家理解上的方便,我們只在page_demo表的行格式演示圖中畫出有關(guān)的頭信息屬性以及c1、c2、c3列的信息(其他信息沒畫不代表它們不存在啊,只 是為了理解上的方便在圖中省略了~),簡化后的行格式示意圖就是這樣:


image.png

下邊我們試著向page_demo表中插入幾條記錄:

mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd'); Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

為了方便大家分析這些記錄在頁的User Records部分中是怎么表示的,我把記錄中頭信息和實(shí)際的列數(shù)據(jù)都用十進(jìn)制表示出來了(其實(shí)是一堆二進(jìn)制位),所以這些記錄的示意圖就是:


image.png

看這個(gè)圖的時(shí)候需要注意一下,各條記錄在User Records中存儲的時(shí)候并沒有空隙,這里只是為了大家觀看方便才把每條記錄單獨(dú)畫在一行中。我們對照著這個(gè)圖來看看記錄頭信息中的各個(gè)屬性是啥意 思:

  • delete_mask

    這個(gè)屬性標(biāo)記著當(dāng)前記錄是否被刪除,占用1個(gè)二進(jìn)制位,值為0的時(shí)候代表記錄并沒有被刪除,為1的時(shí)候代表記錄被刪除掉了。

    啥?被刪除的記錄還在中么?是的,擺在臺面上的和背地里做的可能大相徑庭,你以為它刪除了,可它還在真實(shí)的磁盤上[攤手](忽然想起冠希~)。這些被刪除的記錄之所以不立即從磁盤上移除,是因?yàn)橐瞥鼈冎蟀哑渌挠涗浽诖疟P上重新排列需要性能消耗,所以只是打一個(gè)刪除標(biāo)記而已,所有被刪除掉的記錄都會組成一個(gè)所謂的垃圾鏈表,在這個(gè)鏈表中的記錄占用的空間稱之為所謂的可重用空間,之后如果有新記錄插入到表中的話,可能把這些被刪除的記錄占用的存儲空間覆蓋掉。

小貼士: 將這個(gè)delete_mask位設(shè)置為1和將被刪除的記錄加入到垃圾鏈表中其實(shí)是兩個(gè)階段,我們后邊在介紹事務(wù)的時(shí)候會詳細(xì)嘮叨刪除操作的詳細(xì)過程.

  • min_rec_mask

    B+樹的每層非葉子節(jié)點(diǎn)中的最小記錄都會添加該標(biāo)記,什么是個(gè)B+樹?什么是個(gè)非葉子節(jié)點(diǎn)?好吧,等會再聊這個(gè)問題。反正我們自己插入的四條記錄的min_rec_mask值都是0,意味著它們都不是B+樹的非葉子節(jié)點(diǎn)中的最小記錄。

  • n_owned

    這個(gè)暫時(shí)保密,稍后它是主角~

  • heap_no

    這個(gè)屬性表示當(dāng)前記錄在本中的位置,從圖中可以看出來,我們插入的4條記錄在本中的位置分別是:2345。是不是少了點(diǎn)啥?是的,怎么不見heap_no值為01的記錄呢?

    這其實(shí)是設(shè)計(jì)InnoDB的大叔們玩的一個(gè)小把戲,他們自動(dòng)給每個(gè)頁里邊兒加了兩個(gè)記錄,由于這兩個(gè)記錄并不是我們自己插入的,所以有時(shí)候也稱為偽記錄或者虛擬記錄。這兩個(gè)偽記錄一個(gè)代表最小記錄,一個(gè)代表最大記錄,等一下哈~,記錄可以比大小么?

    是的,記錄也可以比大小,對于<span style="color:red">一條完整的記錄</span>來說,比較記錄的大小就是比較主鍵的大小。比方說我們插入的4行記錄的主鍵值分別是:1234,這也就意味著這4條記錄的大小從小到大依次遞增。

但是不管我們向頁中插入了多少自己的記錄,設(shè)計(jì)InnoDB的大叔們都規(guī)定他們定義的兩條偽記錄分別為最小記錄與最大記錄。這兩條記錄的構(gòu)造十分簡單,都是由5字節(jié)大小的記錄頭信息和8字節(jié)大小
的一個(gè)固定的部分組成的,如圖所示


image.png

由于這兩條記錄不是我們自己定義的記錄,所以它們并不存放在頁的User Records部分,他們被單獨(dú)放在一個(gè)稱為Infimum + Supremum的部分,如圖所示:

image.png

從圖中我們可以看出來,最小記錄和最大記錄的heap_no值分別是0和1,也就是說它們的位置最靠前。

  • record_type

    這個(gè)屬性表示當(dāng)前記錄的類型,一共有4種類型的記錄,0表示普通記錄,1表示B+樹非葉節(jié)點(diǎn)記錄,2表示最小記錄,3表示最大記錄。從圖中我們也可以看出來,我們自己插入的記錄就是普通記錄,它們的record_type值都是0,而最小記錄和最大記錄的record_type值分別為23

    至于record_type1的情況,我們之后在說索引的時(shí)候會重點(diǎn)強(qiáng)調(diào)的。

  • next_record

    這玩意兒非常重要,它表示<span style="color:red">從當(dāng)前記錄的真實(shí)數(shù)據(jù)到下一條記錄的真實(shí)數(shù)據(jù)的地址偏移量</span>。比方說第一條記錄的next_record值為32,意味著從第一條記錄的真實(shí)數(shù)據(jù)的地址處向后找32個(gè)字節(jié)便是下一條記錄的真實(shí)數(shù)據(jù)。如果你熟悉數(shù)據(jù)結(jié)構(gòu)的話,就立即明白了,這其實(shí)是個(gè)鏈表,可以通過一條記錄找到它的下一條記錄。但是需要注意注意再注意的一點(diǎn)是,<span style="color:red">下一條記錄指得并不是按照我們插入順序的下一條記錄,而是按照主鍵值由小到大的順序的下一條記錄</span>。而且規(guī)定Infimum記錄(也就是最小記錄) 的下一條記錄就是本頁中主鍵值最小的用戶記錄,而本頁中主鍵值最大的用戶記錄的下一條記錄就是 Supremum記錄(也就是最大記錄) </span>,為了更形象的表示一下這個(gè)next_record起到的作用,我們用箭頭來替代一下next_record中的地址偏移量:

image.png

從圖中可以看出來,我們的記錄按照主鍵從小到大的順序形成了一個(gè)單鏈表。最大記錄的next_record的值為0,這也就是說最大記錄是沒有下一條記錄了,它是這個(gè)單鏈表中的最后一個(gè)節(jié)點(diǎn)。如果從 中刪除掉一條記錄,這個(gè)鏈表也是會跟著變化的,比如我們把第2條記錄刪掉:

mysql> DELETE FROM page_demo WHERE c1 = 2; Query OK, 1 row affected (0.02 sec)

刪掉第2條記錄后的示意圖就是:


image.png

從圖中可以看出來,刪除第2條記錄前后主要發(fā)生了這些變化:

- 第2條記錄并沒有從存儲空間中移除,而是把該條記錄的`delete_mask`值設(shè)置為`1`。
- 第2條記錄的`next_record`值變?yōu)榱?,意味著該記錄沒有下一條記錄了。
- 第1條記錄的`next_record`指向了第3條記錄。
- 還有一點(diǎn)你可能忽略了,就是`最大記錄`的`n_owned`值從`5`變成了`4`,關(guān)于這一點(diǎn)的變化我們稍后會詳細(xì)說明的。

所以,不論我們怎么對頁中的記錄做增刪改操作,InnoDB始終會維護(hù)一條記錄的單鏈表,鏈表中的各個(gè)節(jié)點(diǎn)是按照主鍵值由小到大的順序連接起來的。

小貼士: 你會不會覺得next_record這個(gè)指針有點(diǎn)兒怪,為啥要指向記錄頭信息和真實(shí)數(shù)據(jù)之間的位置呢?為啥不干脆指向整條記錄的開頭位置,也就是記錄的額外信息開頭的位置呢? 因 為這個(gè)位置剛剛好,向左讀取就是記錄頭信息,向右讀取就是真實(shí)數(shù)據(jù)。我們前邊還說過變長字段長度列表、NULL值列表中的信息都是逆序存放,這樣可以使記錄中位置靠前的字段和 它們對應(yīng)的字段長度信息在內(nèi)存中的距離更近,可能會提高高速緩存的命中率。當(dāng)然如果你看不懂這句話的話就不要勉強(qiáng)了,果斷跳過~

再來看一個(gè)有意思的事情,因?yàn)橹麈I值為2的記錄被我們刪掉了,但是存儲空間卻沒有回收,如果我們再次把這條記錄插入到表中,會發(fā)生什么事呢?

mysql> INSERT INTO page_demo VALUES(2, 200, 'bbbb');
Query OK, 1 row affected (0.00 sec)

我們看一下記錄的存儲情況:


image.png

從圖中可以看到,InnoDB并沒有因?yàn)樾掠涗浀牟迦攵鵀樗暾埿碌拇鎯臻g,而是直接復(fù)用了原來被刪除記錄的存儲空間。
小貼士: 當(dāng)數(shù)據(jù)頁中存在多條被刪除掉的記錄時(shí),這些記錄的next_record屬性將會把這些被刪除掉的記錄組成一個(gè)垃圾鏈表,以備之后重用這部分存儲空間。

Page Directory(頁目錄)

現(xiàn)在我們了解了記錄在頁中按照主鍵值由小到大順序串聯(lián)成一個(gè)單鏈表,那如果我們想根據(jù)主鍵值查找頁中的某條記錄該咋辦呢?比如說這樣的查詢語句:

SELECT * FROM page_demo WHERE c1 = 3;

最笨的辦法:從Infimum記錄(最小記錄)開始,沿著鏈表一直往后找,總有一天會找到(或者找不到[攤手]),在找的時(shí)候還能投機(jī)取巧,因?yàn)殒湵碇懈鱾€(gè)記錄的值是按照從小到大順序排列的,所以當(dāng)
鏈表的某個(gè)節(jié)點(diǎn)代表的記錄的主鍵值大于你想要查找的主鍵值時(shí),你就可以停止查找了,因?yàn)樵摴?jié)點(diǎn)后邊的節(jié)點(diǎn)的主鍵值依次遞增。

這個(gè)方法在頁中存儲的記錄數(shù)量比較少的情況用起來也沒啥問題,比方說現(xiàn)在我們的表里只有4條自己插入的記錄,所以最多找4次就可以把所有記錄都遍歷一遍,但是如果一個(gè)頁中存儲了非常多的記 錄,這么查找對性能來說還是有損耗的,所以我們說這種遍歷查找這是一個(gè)笨辦法。但是設(shè)計(jì)InnoDB的大叔們是什么人,他們能用這么笨的辦法么,當(dāng)然是要設(shè)計(jì)一種更6的查找方式嘍,他們從書的目錄 中找到了靈感。

我們平常想從一本書中查找某個(gè)內(nèi)容的時(shí)候,一般會先看目錄,找到需要查找的內(nèi)容對應(yīng)的書的頁碼,然后到對應(yīng)的頁碼查看內(nèi)容。設(shè)計(jì)InnoDB的大叔們?yōu)槲覀兊挠涗浺仓谱髁艘粋€(gè)類似的目錄,他們的 制作過程是這樣的:

  1. 將所有正常的記錄(包括最大和最小記錄,不包括標(biāo)記為已刪除的記錄)劃分為幾個(gè)組。
  2. 每個(gè)組的最后一條記錄(也就是組內(nèi)最大的那條記錄)的頭信息中的n_owned屬性表示該記錄擁有多少條記錄,也就是該組內(nèi)共有幾條記錄。
  3. 將每個(gè)組的最后一條記錄的地址偏移量單獨(dú)提取出來按順序存儲到靠近頁的尾部的地方,這個(gè)地方就是所謂的Page Directory,也就是頁目錄(此時(shí)應(yīng)該返回頭看看頁面各個(gè)部分的圖)。頁面目錄 中的這些地址偏移量被稱為槽(英文名:Slot),所以這個(gè)頁面目錄就是由槽組成的。

比方說現(xiàn)在的page_demo表中正常的記錄共有6條,InnoDB會把它們分成兩組,第一組中只有一個(gè)最小記錄,第二組中是剩余的5條記錄,看下邊的示意圖:


image.png
  • 現(xiàn)在頁目錄部分中有兩個(gè)槽,也就意味著我們的記錄被分成了兩個(gè)組,槽1中的值是112,代表最大記錄的地址偏移量(就是從頁面的0字節(jié)開始數(shù),數(shù)112個(gè)字節(jié));槽0中的值是99,代表最小記錄的地址偏移量。

  • 注意最小和最大記錄的頭信息中的n_owned屬性

    • 最小記錄的n_owned值為1,這就代表著以最小記錄結(jié)尾的這個(gè)分組中只有1條記錄,也就是最小記錄本身。
    • 最大記錄的n_owned值為5,這就代表著以最大記錄結(jié)尾的這個(gè)分組中只有5條記錄,包括最大記錄本身還有我們自己插入的4條記錄。

99112這樣的地址偏移量很不直觀,我們用箭頭指向的方式替代數(shù)字,這樣更易于我們理解,所以修改后的示意圖就是這樣:

image.png

哎呀,咋看上去怪怪的,這么亂的圖對于我這個(gè)強(qiáng)迫癥真是不能忍,那我們就暫時(shí)不管各條記錄在存儲設(shè)備上的排列方式了,單純從邏輯上看一下這些記錄和頁目錄的關(guān)系:


image.png

這樣看就順眼多了嘛!為什么最小記錄的n_owned值為1,而最大記錄的n_owned值為5呢,這里頭有什么貓膩么?

是的,設(shè)計(jì)InnoDB的大叔們對每個(gè)分組中的記錄條數(shù)是有規(guī)定的:對于最小記錄所在的分組只能有 1 條記錄,最大記錄所在的分組擁有的記錄條數(shù)只能在 1~8 條之間,剩下的分組中記錄的條數(shù)范圍只能在是 4~8 條之間。所以分組是按照下邊的步驟進(jìn)行的:

  • 初始情況下一個(gè)數(shù)據(jù)頁里只有最小記錄和最大記錄兩條記錄,它們分屬于兩個(gè)分組。

  • 之后每插入一條記錄,都會從頁目錄中找到主鍵值比本記錄的主鍵值大并且差值最小的槽,然后把該槽對應(yīng)的記錄的n_owned值加1,表示本組內(nèi)又添加了一條記錄,直到該組中的記錄數(shù)等于8個(gè)。

  • 在一個(gè)組中的記錄數(shù)等于8個(gè)后再插入一條記錄時(shí),會將組中的記錄拆分成兩個(gè)組,一個(gè)組中4條記錄,另一個(gè)5條記錄。這個(gè)過程會在頁目錄中新增一個(gè)來記錄這個(gè)新增分組中最大的那條記錄的偏移量。

由于現(xiàn)在page_demo表中的記錄太少,無法演示添加了頁目錄之后加快查找速度的過程,所以再往page_demo表中添加一些記錄:

mysql> INSERT INTO page_demo VALUES(5, 500, 'eeee'), (6, 600, 'ffff'), (7, 700, 'gggg'), (8, 800, 'hhhh'), (9, 900, 'iiii'), (10, 1000, 'jjjj'), (11, 1100, 'kkkk'), (12, 1200, 'llll'), (13, 1300, 'mmmm'), (14, 1400, 'nnnn'), (15, 1500, 'oooo'), (16, 1600, 'pppp');
Query OK, 12 rows affected (0.00 sec)
Records: 12  Duplicates: 0  Warnings: 0

哈,我們一口氣又往表中添加了12條記錄,現(xiàn)在頁里邊就一共有18條記錄了(包括最小和最大記錄),這些記錄被分成了5個(gè)組,如圖所示:


image.png

因?yàn)榘?6條記錄的全部信息都畫在一張圖里太占地方,讓人眼花繚亂的,所以只保留了用戶記錄頭信息中的n_owned和next_record屬性,也省略了各個(gè)記錄之間的箭頭,我沒畫不等于沒有啊!現(xiàn)在看怎 么從這個(gè)頁目錄中查找記錄。因?yàn)楦鱾€(gè)槽代表的記錄的主鍵值都是從小到大排序的,所以我們可以使用所謂的二分法來進(jìn)行快速查找。4個(gè)槽的編號分別是:0、1、2、3、4,所以初始情況下最低的槽就 是low=0,最高的槽就是high=4。比方說我們想找主鍵值為6的記錄,過程是這樣的:

  1. 計(jì)算中間槽的位置:(0+4)/2=2,所以查看槽2對應(yīng)記錄的主鍵值為8,又因?yàn)?code>8 > 6,所以設(shè)置high=2low保持不變。

  2. 重新計(jì)算中間槽的位置:(0+2)/2=1,所以查看槽1對應(yīng)的主鍵值為4,又因?yàn)?code>4 < 6,所以設(shè)置low=1high保持不變。

  3. 因?yàn)?code>high - low的值為1,所以確定主鍵值為6的記錄在槽2對應(yīng)的組中。此刻我們需要找到槽2中主鍵值最小的那條記錄,然后沿著單向鏈表遍歷槽2中的記錄。但是我們前邊又說過,每個(gè)槽對應(yīng)的記錄都是該組中主鍵值最大的記錄,這里槽2對應(yīng)的記錄是主鍵值為8的記錄,怎么定位一個(gè)組中最小的記錄呢?別忘了各個(gè)槽都是挨著的,我們可以很輕易的拿到槽1對應(yīng)的記錄(主鍵值為4),該條記錄的下一條記錄就是槽2中主鍵值最小的記錄,該記錄的主鍵值為5。所以我們可以從這條主鍵值為5的記錄出發(fā),遍歷槽2中的各條記錄,直到找到主鍵值為6的那條記錄即可。由于一個(gè)組中包含的記錄條數(shù)只能是1~8條,所以遍歷一個(gè)組中的記錄的代價(jià)是很小的。

所以在一個(gè)數(shù)據(jù)頁中查找指定主鍵值的記錄的過程分為兩步:

  1. 通過二分法確定該記錄所在的槽,并找到該槽中主鍵值最小的那條記錄。

  2. 通過記錄的next_record屬性遍歷該槽所在的組中的各個(gè)記錄。

Page Header(頁面頭部)

設(shè)計(jì)InnoDB的大叔們?yōu)榱四艿玫揭粋€(gè)數(shù)據(jù)頁中存儲的記錄的狀態(tài)信息,比如本頁中已經(jīng)存儲了多少條記錄,第一條記錄的地址是什么,頁目錄中存儲了多少個(gè)槽等等,特意在頁中定義了一個(gè)叫Page Header的部分,它是頁結(jié)構(gòu)的第二部分,這個(gè)部分占用固定的56個(gè)字節(jié),專門存儲各種狀態(tài)信息,具體各個(gè)字節(jié)都是干嘛的看下表:

名稱 占用空間大小 描述
PAGE_N_DIR_SLOTS 2字節(jié) 在頁目錄中的槽數(shù)量
PAGE_HEAP_TOP 2字節(jié) 還未使用的空間最小地址,也就是說從該地址之后就是Free Space
PAGE_N_HEAP 2字節(jié) 本頁中的記錄的數(shù)量(包括最小和最大記錄以及標(biāo)記為刪除的記錄)
PAGE_FREE 2字節(jié) 第一個(gè)已經(jīng)標(biāo)記為刪除的記錄地址(各個(gè)已刪除的記錄通過next_record也會組成一個(gè)單鏈表,這個(gè)單鏈表中的記錄可以被重新利用)
PAGE_GARBAGE 2字節(jié) 已刪除記錄占用的字節(jié)數(shù)
PAGE_LAST_INSERT 2字節(jié) 最后插入記錄的位置
PAGE_DIRECTION 2字節(jié) 記錄插入的方向
PAGE_N_DIRECTION 2字節(jié) 一個(gè)方向連續(xù)插入的記錄數(shù)量
PAGE_N_RECS 2字節(jié) 該頁中記錄的數(shù)量(不包括最小和最大記錄以及被標(biāo)記為刪除的記錄)
PAGE_MAX_TRX_ID 8字節(jié) 修改當(dāng)前頁的最大事務(wù)ID,該值僅在二級索引中定義
PAGE_LEVEL 2字節(jié) 當(dāng)前頁在B+樹中所處的層級
PAGE_INDEX_ID 8字節(jié) 索引ID,表示當(dāng)前頁屬于哪個(gè)索引
PAGE_BTR_SEG_LEAF 10字節(jié) B+樹葉子段的頭部信息,僅在B+樹的Root頁定義
PAGE_BTR_SEG_TOP 10字節(jié) B+樹非葉子段的頭部信息,僅在B+樹的Root頁定義

在這里我們先嘮叨一下PAGE_DIRECTIONPAGE_N_DIRECTION的意思:

  • PAGE_DIRECTION

    假如新插入的一條記錄的主鍵值比上一條記錄的主鍵值大,我們說這條記錄的插入方向是右邊,反之則是左邊。用來表示最后一條記錄插入方向的狀態(tài)就是PAGE_DIRECTION

  • PAGE_N_DIRECTION

    假設(shè)連續(xù)幾次插入新記錄的方向都是一致的,InnoDB會把沿著同一個(gè)方向插入記錄的條數(shù)記下來,這個(gè)條數(shù)就用PAGE_N_DIRECTION這個(gè)狀態(tài)表示。當(dāng)然,如果最后一條記錄的插入方向改變了的話,這個(gè)狀態(tài)的值會被清零重新統(tǒng)計(jì)。

File Header(文件頭部)

上邊嘮叨的Page Header是專門針對數(shù)據(jù)頁記錄的各種狀態(tài)信息,比方說頁里頭有多少個(gè)記錄了呀,有多少個(gè)槽了呀。我們現(xiàn)在描述的File Header針對各種類型的頁都通用,也就是說不同類型的頁都會 以File Header作為第一個(gè)組成部分,它描述了一些針對各種頁都通用的一些信息,比方說這個(gè)頁的編號是多少,它的上一個(gè)頁、下一個(gè)頁是誰啦吧啦吧啦~ 這個(gè)部分占用固定的38個(gè)字節(jié),是由下邊這 些內(nèi)容組成的:

名稱 占用空間大小 描述
FIL_PAGE_SPACE_OR_CHKSUM 4字節(jié) 頁的校驗(yàn)和(checksum值)
FIL_PAGE_OFFSET 4字節(jié) 頁號
FIL_PAGE_PREV 4字節(jié) 上一個(gè)頁的頁號
FIL_PAGE_NEXT 4字節(jié) 下一個(gè)頁的頁號
FIL_PAGE_LSN 8字節(jié) 頁面被最后修改時(shí)對應(yīng)的日志序列位置(英文名是:Log Sequence Number)
FIL_PAGE_TYPE 2字節(jié) 該頁的類型
FIL_PAGE_FILE_FLUSH_LSN 8字節(jié) 僅在系統(tǒng)表空間的一個(gè)頁中定義,代表文件至少被刷新到了對應(yīng)的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4字節(jié) 頁屬于哪個(gè)表空間

對照著這個(gè)表格,我們看幾個(gè)目前比較重要的部分:

  • FIL_PAGE_SPACE_OR_CHKSUM

    這個(gè)代表當(dāng)前頁面的校驗(yàn)和(checksum)。啥是個(gè)校驗(yàn)和?就是對于一個(gè)很長很長的字節(jié)串來說,我們會通過某種算法來計(jì)算一個(gè)比較短的值來代表這個(gè)很長的字節(jié)串,這個(gè)比較短的值就稱為校驗(yàn)和。這樣在比較兩個(gè)很長的字節(jié)串之前先比較這兩個(gè)長字節(jié)串的校驗(yàn)和,如果校驗(yàn)和都不一樣兩個(gè)長字節(jié)串肯定是不同的,所以省去了直接比較兩個(gè)比較長的字節(jié)串的時(shí)間損耗。

  • FIL_PAGE_OFFSET

    每一個(gè)都有一個(gè)單獨(dú)的頁號,就跟你的身份證號碼一樣,InnoDB通過頁號來可以唯一定位一個(gè)

  • FIL_PAGE_TYPE

    這個(gè)代表當(dāng)前的類型,我們前邊說過,InnoDB為了不同的目的而把頁分為不同的類型,我們上邊介紹的其實(shí)都是存儲記錄的數(shù)據(jù)頁,其實(shí)還有很多別的類型的頁,具體如下表:

    類型名稱 十六進(jìn)制 描述
    FIL_PAGE_TYPE_ALLOCATED 0x0000 最新分配,還沒使用
    FIL_PAGE_UNDO_LOG 0x0002 Undo日志頁
    FIL_PAGE_INODE 0x0003 段信息節(jié)點(diǎn)
    FIL_PAGE_IBUF_FREE_LIST 0x0004 Insert Buffer空閑列表
    FIL_PAGE_IBUF_BITMAP 0x0005 Insert Buffer位圖
    FIL_PAGE_TYPE_SYS 0x0006 系統(tǒng)頁
    FIL_PAGE_TYPE_TRX_SYS 0x0007 事務(wù)系統(tǒng)數(shù)據(jù)
    FIL_PAGE_TYPE_FSP_HDR 0x0008 表空間頭部信息
    FIL_PAGE_TYPE_XDES 0x0009 擴(kuò)展描述頁
    FIL_PAGE_TYPE_BLOB 0x000A BLOB頁
    FIL_PAGE_INDEX 0x45BF 索引頁,也就是我們所說的數(shù)據(jù)頁

    我們存放記錄的數(shù)據(jù)頁的類型其實(shí)是FIL_PAGE_INDEX,也就是所謂的索引頁

  • FIL_PAGE_PREVFIL_PAGE_NEXT

    我們前邊強(qiáng)調(diào)過,InnoDB都是以頁為單位存放數(shù)據(jù)的,有時(shí)候我們存放某種類型的數(shù)據(jù)占用的空間非常大(比方說一張表中可以有成千上萬條記錄),InnoDB可能不可以一次性為這么多數(shù)據(jù)分配一個(gè)非常大的存儲空間,如果分散到多個(gè)不連續(xù)的頁中存儲的話需要把這些頁關(guān)聯(lián)起來,FIL_PAGE_PREVFIL_PAGE_NEXT就分別代表本頁的上一個(gè)和下一個(gè)頁的頁號。這樣通過建立一個(gè)雙向鏈表把許許多多的頁就都串聯(lián)起來了,而無需這些頁在物理上真正連著。需要注意的是,<span style="color:red">并不是所有類型的頁都有上一個(gè)和下一個(gè)頁的屬性</span>,不過我們本集中嘮叨的數(shù)據(jù)頁(也就是類型為FIL_PAGE_INDEX的頁)是有這兩個(gè)屬性的,所以所有的數(shù)據(jù)頁其實(shí)是一個(gè)雙鏈表,就像這樣:

    image.png

File Trailer

我們知道InnoDB存儲引擎會把數(shù)據(jù)存儲到磁盤上,但是磁盤速度太慢,需要以為單位把數(shù)據(jù)加載到內(nèi)存中處理,如果該頁中的數(shù)據(jù)在內(nèi)存中被修改了,那么在修改后的某個(gè)時(shí)間需要把數(shù)據(jù)<span style="color:red">同步</span>到磁盤中。但是在同步了一半的時(shí)候中斷電了咋辦,這不是莫名尷尬么?為了檢測一個(gè)頁是否完整(也就是在同步的時(shí)候有沒有發(fā)生只同步一半的尷尬情況),設(shè)計(jì)InnoDB的大叔們在每個(gè)頁的尾部都加了一個(gè)File Trailer部分,這個(gè)部分由8個(gè)字節(jié)組成,可以分成2個(gè)小部分:

  • 前4個(gè)字節(jié)代表頁的校驗(yàn)和

    這個(gè)部分是和File Header中的校驗(yàn)和相對應(yīng)的。每當(dāng)一個(gè)頁面在內(nèi)存中修改了,在同步之前就要把它的校驗(yàn)和算出來,因?yàn)?code>File Header在頁面的前邊,所以校驗(yàn)和會被首先同步到磁盤,當(dāng)完全寫完時(shí),校驗(yàn)和也會被寫到頁的尾部,如果完全同步成功,則頁的首部和尾部的校驗(yàn)和應(yīng)該是一致的。如果寫了一半兒斷電了,那么在File Header中的校驗(yàn)和就代表著已經(jīng)修改過的頁,而在File Trialer中的校驗(yàn)和代表著原先的頁,二者不同則意味著同步中間出了錯(cuò)。

  • 后4個(gè)字節(jié)代表頁面被最后修改時(shí)對應(yīng)的日志序列位置(LSN)

    這個(gè)部分也是為了校驗(yàn)頁的完整性的,只不過我們目前還沒說LSN是個(gè)什么意思,所以大家可以先不用管這個(gè)屬性。

這個(gè)File TrailerFile Header類似,都是所有類型的頁通用的。

總結(jié)

  1. InnoDB為了不同的目的而設(shè)計(jì)了不同類型的頁,我們把用于存放記錄的頁叫做數(shù)據(jù)頁

  2. 一個(gè)數(shù)據(jù)頁可以被大致劃分為7個(gè)部分,分別是

    • File Header,表示頁的一些通用信息,占固定的38字節(jié)。
    • Page Header,表示數(shù)據(jù)頁專有的一些信息,占固定的56個(gè)字節(jié)。
    • Infimum + Supremum,兩個(gè)虛擬的偽記錄,分別表示頁中的最小和最大記錄,占固定的26個(gè)字節(jié)。
    • User Records:真實(shí)存儲我們插入的記錄的部分,大小不固定。
    • Free Space:頁中尚未使用的部分,大小不確定。
    • Page Directory:頁中的某些記錄相對位置,也就是各個(gè)槽在頁面中的地址偏移量,大小不固定,插入的記錄越多,這個(gè)部分占用的空間越多。
    • File Trailer:用于檢驗(yàn)頁是否完整的部分,占用固定的8個(gè)字節(jié)。
  3. 每個(gè)記錄的頭信息中都有一個(gè)next_record屬性,從而使頁中的所有記錄串聯(lián)成一個(gè)單鏈表

  4. InnoDB會為把頁中的記錄劃分為若干個(gè)組,每個(gè)組的最后一個(gè)記錄的地址偏移量作為一個(gè),存放在Page Directory中,所以在一個(gè)頁中根據(jù)主鍵查找記錄是非常快的,分為兩步:

    • 通過二分法確定該記錄所在的槽。

    • 通過記錄的next_record屬性遍歷該槽所在的組中的各個(gè)記錄。

  5. 每個(gè)數(shù)據(jù)頁的File Header部分都有上一個(gè)和下一個(gè)頁的編號,所以所有的數(shù)據(jù)頁會組成一個(gè)雙鏈表

  6. 為保證從內(nèi)存中同步到磁盤的頁的完整性,在頁的首部和尾部都會存儲頁中數(shù)據(jù)的校驗(yàn)和和頁面最后修改時(shí)對應(yīng)的LSN值,如果首部和尾部的校驗(yàn)和和LSN值校驗(yàn)不成功的話,就說明同步過程出現(xiàn)了問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。