一文搞懂MySQL索引頁結(jié)構(gòu)
1. 前言
「頁」是InnoDB管理存儲空間的基本單位,也是內(nèi)存和磁盤交互的基本單位。也就是說,哪怕你需要1字節(jié)的數(shù)據(jù),InnoDB也會讀取整個頁的數(shù)據(jù),下次讀取的數(shù)據(jù)如果恰巧也在這個頁里,就能命中緩存了。寫也是一樣的,寫數(shù)據(jù)前要先把頁加載到內(nèi)存,然后在內(nèi)存中修改,該頁被記為「臟頁」,臟頁淘汰之前必須刷盤。
InnoDB有很多類型的頁,它們的用處也各不相同。比如:有存放undo日志的頁、有存放INODE信息的頁、有存放Change Buffer信息的頁、存放用戶記錄數(shù)據(jù)的頁等等。今天我們要聊的,就是最基礎(chǔ)也是最重要的,存放用戶記錄數(shù)據(jù)的「索引頁」。
2. 索引頁結(jié)構(gòu)
InnoDB默認(rèn)的頁大小是16KB,在初始化表空間之前可以在配置文件中進(jìn)行配置,一旦初始化完成就不可再變更了。查看頁大小的命令如下,顯示的是字節(jié)數(shù)。
SHOW VARIABLES LIKE 'innodb_page_size';
索引頁結(jié)構(gòu)如下圖所示:
索引頁由七部分組成,其中Infimum和Supremum也屬于記錄,只不過是虛擬記錄,這里為了與用戶記錄區(qū)分開,還是決定將兩者拆開。
名稱 | 大小 | 描述 |
---|---|---|
File Header | 38字節(jié) | 所有頁的通用文件頭信息 |
Page Header | 56字節(jié) | 索引頁特有的頁頭信息 |
Infimum+Supremum | 26字節(jié) | 頁中虛擬的最小、最大記錄 |
User Records | 變長 | 用戶記錄數(shù)據(jù) |
Free Space | 變長 | 空閑空間 |
Page Directory | 變長 | 頁目錄,加速頁內(nèi)數(shù)據(jù)檢索效率 |
File Trailer | 8字節(jié) | 所有頁的通用文件尾信息,校驗頁是否完整 |
2.1 File Header
File Header是所有頁都有的一個通用的結(jié)構(gòu),占用固定的38字節(jié),它記錄了頁的一些通用的狀態(tài)信息,例如:頁的頁號、Checksum、把頁串聯(lián)成雙向鏈表的指針、頁的類型等等。
名稱 | 大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHECKSUM | 4字節(jié) | 新版本中代表頁的校驗和Checksum |
FIL_PAGE_OFFSET | 4字節(jié) | 頁號 |
FIL_PAGE_PREV | 4字節(jié) | 上一個頁的頁號 |
FIL_PAGE_NEXT | 4字節(jié) | 下一個頁的頁號 |
FIL_PAGE_LSN | 8字節(jié) | 頁面最后被修改時的LSN值 |
FIL_PAGE_TYPE | 2字節(jié) | 頁的類型 |
FIL_PAGE_FILE_FLUSH_LSN | 8字節(jié) | 僅在系統(tǒng)表空間的第1個頁中使用,代表文件至少被刷新到了對應(yīng)的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字節(jié) | 頁數(shù)據(jù)哪個表空間 |
FIL_PAGE_SPACE_OR_CHECKSUM
基于當(dāng)前頁計算出的校驗和(Checksum),可以把它看作是哈希值,校驗和不同,則兩個頁數(shù)據(jù)肯定不同。它的作用是InnoDB在臟頁刷盤時,有可能會遇到頁刷到一半斷電的情況,頁的頭和尾部分分別記錄校驗和,只有當(dāng)頭尾的校驗和一致的時候,才代表磁盤上的頁是完整的,否則就是一個損壞的頁。
FIL_PAGE_OFFSET
頁號,頁的唯一標(biāo)識,全局遞增的數(shù)字,InnoDB通過頁號來定位唯一的一個頁。4字節(jié)存儲,意味著一個表空間最多可以有232個頁,按照一個頁16KB計算,則一個表空間最多支持64TB的數(shù)據(jù)。
FIL_PAGE_PREV & FIL_PAGE_NEXT
一個頁大小才16KB,一張表數(shù)據(jù)其實是由N多個頁構(gòu)成的,頁與頁之間在物理上可以是不連續(xù)的,但是邏輯上要連續(xù),F(xiàn)IL_PAGE_PREV和FIL_PAGE_NEXT分別指向當(dāng)前頁的上一個頁和下一個頁的頁號,通過這兩個指針將索引頁串聯(lián)成了一個雙向鏈表。記錄與記錄之間是單向的,頁與頁之間是雙向的!
FIL_PAGE_LSN
頁面最后被修改時,對應(yīng)的LSN值。LSN的全稱是Log Sequence Number,日志序列號。它是一個遞增的數(shù)字,和事務(wù)相關(guān),這里不作贅述。
FIL_PAGE_TYPE
當(dāng)前頁的類型,InnoDB為了不同的目的設(shè)計了很多不同類型的頁,索引頁的固定值是0x45BF
。
FIL_PAGE_FILE_FLUSH_LSN
僅在第1個頁中使用,用來判斷數(shù)據(jù)庫是正常關(guān)閉還是異常宕機(jī)。
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
僅記錄當(dāng)前頁數(shù)據(jù)哪個表空間。
2.2 Page Header
Page Header是索引頁特有的結(jié)構(gòu),占用固定的56字節(jié),它記錄了索引頁中記錄相關(guān)的狀態(tài)信息。
名稱 | 大小 | 描述 |
---|---|---|
PAGE_N_DlR_SLOTS | 2字節(jié) | 頁目錄中的槽數(shù)量 |
PAGE_HEAP_TOP | 2字節(jié) | 未使用的空間最小地址,User Records和Free Space分界點 |
PAGE_N_HEAP | 2字節(jié) | 本頁中的記錄的數(shù)量(包括虛擬記錄和刪除記錄) |
PAGE_FREE | 2字節(jié) | 第一個刪除的記錄地址,后續(xù)刪除的記錄會形成鏈表。 |
PAGE_GARBAGE | 2字節(jié) | 已刪除記錄占用的字節(jié)數(shù) |
PAGE_LAST_INSERT | 2字節(jié) | 最后插入記錄的位置 |
PAGE_DIRECTION | 2字節(jié) | 記錄插入的方向 |
PAGE_N_DIRECTION | 2字節(jié) | 同一個方向連續(xù)插入的記錄數(shù)量 |
PAGE_N_RECS | 2字節(jié) | 該頁中記錄的數(shù)量(不包括虛擬記錄和刪除記錄) |
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)前頁屬于哪個索引 |
PAGE_BTR_SEG_LEAF | 10字節(jié) | B+樹葉子段的頭部信息,僅在B+樹的Root頁定義 |
PAGE_BTR_SEG_TOP | 10字節(jié) | B+樹非葉子段的頭部信息,僅在B+樹的Root頁定義 |
不用每個屬性都了解,我們挑幾個比較重要的看看。
PAGE_N_DlR_SLOTS
一個頁內(nèi)可能有上千條記錄,挨個遍歷的話效率太慢了。為了提高頁內(nèi)記錄的檢索效率,InnoDB將頁內(nèi)的記錄劃分為多個組,組里最大的那條記錄相較于頁的地址偏移量會記錄到「Page Directory」部分,每個組都對應(yīng)一個槽,槽的大小是固定的2字節(jié)。該屬性記錄的就是頁內(nèi)槽的數(shù)量。
PAGE_HEAP_TOP
Free Space的起始位置,它是User Records和Free Space分界點。一個全新的頁一開始是沒有User Records部分的,每插入一條記錄,都要向Free Space申請空間,F(xiàn)ree Space耗盡就代表頁滿了。
PAGE_FREE
DELETE命令刪除記錄時,InnoDB并不會真的將記錄從磁盤中刪除,而是在記錄的頭信息里打個標(biāo)記,然后將其加入到「垃圾鏈表」中。PAGE_FREE指向的就是垃圾鏈表的表頭記錄。后面刪除的記錄,也會自動加入到鏈表里。
PAGE_DIRECTION & PAGE_N_DIRECTION
PAGE_DIRECTION表示最后一條記錄插入的方向,比上一條記錄值大則記為右邊,反之則是左邊。PAGE_N_DIRECTION表示同一方向連續(xù)插入的記錄數(shù),方向變了該值就會重置。
PAGE_LEVEL
InnoDB組織數(shù)據(jù)的形式就是B+樹,樹中的節(jié)點就是索引頁,PAGE_LEVEL代表當(dāng)前頁在B+樹中所處的層級。InnoDB規(guī)定,葉子節(jié)點層級為0,然后向上遞增。
2.3 User Records
Infimum和Supremum也屬于記錄,只是為了與用戶記錄區(qū)分開才劃分成了兩部分,我們先看User Records。
用戶記錄存放在User Records部分,一個全新的頁一開始全是Free Space,是沒有User Records部分的。每插入一條記錄都需要到Free Space申請一塊空間,并將其劃分到User Records用來存放用戶記錄。當(dāng)Free Space耗盡也就代表當(dāng)前頁已經(jīng)用完了,再有新記錄需要插入,就需要申請一個新的頁了。
還記得MySQL的行格式嗎?它決定了記錄在磁盤里的存儲格式。以COMPACT為例,存儲格式如下圖:
記錄頭信息里的字段比較關(guān)鍵,以防大家忘記,我這里再貼一下:
名稱 | 大小(Bit) | 說明 |
---|---|---|
預(yù)留位1 | 1 | 沒有使用 |
預(yù)留位2 | 1 | 沒有使用 |
deleted_flag | 1 | 記錄刪除標(biāo)記 |
min_rec_flag | 1 | B+樹非葉子節(jié)點的最小目錄項標(biāo)記 |
n_owned | 4 | 同一頁內(nèi)同一組里最大的記錄會記錄組里的記錄數(shù)量,其余記錄該值為0 |
heap_no | 13 | 當(dāng)前記錄在頁面堆里的相對位置 |
record_type | 3 | 記錄類型。0:普通記錄,1:B+樹非葉子節(jié)點目錄項記錄,2:Infimum記錄,3:Supremum記錄. |
next_record | 16 | 下一條記錄的相對位置 |
記錄頭信息的最后2字節(jié)用來連接下一條記錄,將頁內(nèi)所有記錄串聯(lián)成一個單向鏈表。所以我們隱藏變長字段長度列表和NULL值列表,記錄的格式應(yīng)該是這樣的:
記錄是怎么排序的?
我們已經(jīng)知道,頁內(nèi)的記錄會自動串聯(lián)成一個單向鏈表。那這個鏈表的編排順序是什么呢?是按照記錄的插入時間排序的嗎?其實不是的,如果表有主鍵,會根據(jù)主鍵排序;沒主鍵有唯一非空索引,會根據(jù)該索引排序;兩者都沒有,InnoDB會自動生成一個row_id
列并根據(jù)該列進(jìn)行排序。
若無特殊說明,本文均假定表有主鍵。
2.4 Infimum & Supremum
Infimum和Supremum是索引頁內(nèi)的兩條虛擬記錄,InnoDB規(guī)定所有索引頁都會有這兩條記錄,而且所有的用戶記錄都比Infimum大,都比Supremum小。
記錄頭信息里的heap_no代表記錄在堆里的相對位置,該值越小代表記錄越靠前。細(xì)心的同學(xué)會發(fā)現(xiàn),上圖中的用戶記錄heap_no值是從2開始的,那0和1呢?不說你也肯定猜到了,就是被Infimum和Supremum占用了。Infimum和Supremum的heap_no值分別是0和1,它倆在所有用戶記錄的最前面。
Infimum和Supremum結(jié)構(gòu)非常的簡單,和用戶記錄一樣也有頭信息,真實數(shù)據(jù)部分是固定的字符串,如下圖所示:
我們把這兩條虛擬記錄也加入到記錄里面,完整的結(jié)構(gòu)就是下面這樣的:
Supremum記錄的next_record屬性為0,代表它已經(jīng)沒有下一條記錄了。
2.5 Page Directory
Free Space沒什么好說的,就是一塊未被使用的空閑空間。
Page Directory也叫作「頁目錄」,它的目的是提高頁內(nèi)記錄的檢索效率。相較于一張表幾千萬的記錄來說,一個頁內(nèi)幾百上千條記錄已經(jīng)是很少很少了??杉幢闳绱?,它也有幾百上千條啊,如果頁內(nèi)檢索記錄只能挨個遍歷的話,那也太低效了。別忘了,頁內(nèi)的記錄是根據(jù)索引值排好序的,我們可以巧用「二分法」來快速查找。
具體做法是:將頁內(nèi)所有非刪除的記錄劃分為N個組,每個組里最后一條記錄(即主鍵最大的記錄)稱作“大哥”,其余記錄是“小弟”,“大哥”的n_owned
屬性記錄了組內(nèi)的記錄數(shù)量。將“大哥”在頁內(nèi)的地址偏移量提取出來,按順序依次從File Trailer部分往前寫,每個地址偏移量占用2字節(jié),稱作一個「槽」,Page Directory就是由這些槽構(gòu)成的。
InnoDB對于分組內(nèi)的記錄數(shù)量有一些規(guī)定:
- Infimum記錄所在分組,只能有一條記錄。
- Supremum記錄所在分組,允許有1~8條記錄。
- 其余分組,允許有4~8條記錄。
由此可見,一個組里最多有8條記錄,只要通過二分法快速定位到組,InnoDB也只需要遍歷這8條記錄,相較于遍歷頁內(nèi)所有記錄,效率要高的多。
2.6 File Trailer
File Trailer是所有頁都有的通用結(jié)構(gòu),占用固定的8字節(jié),它的主要作用就是為了校驗頁的完整性。磁盤的速度實在是太慢了,InnoDB不會每次寫點數(shù)據(jù)都直接刷新到磁盤上,那樣MySQL會慢死。而是將頁作為刷盤的基本單位,數(shù)據(jù)修改時,先改內(nèi)存里的頁,稍后再將整個頁的數(shù)據(jù)一次性刷新到磁盤里。但是這會帶來一個問題,一個頁16KB,刷到第10KB的時候磁盤斷電了怎么辦?重啟后InnoDB如何判斷磁盤里的頁數(shù)據(jù)是完整的?
?
InnoDB是這么處理的,刷盤前根據(jù)頁數(shù)據(jù)計算出一個Checksum,在頁頭和頁尾都寫一份。頁刷盤的時候,先刷頁頭再刷頁尾,當(dāng)頭尾兩個Checksum值一致的時候,代表磁盤里的頁是完整的,否則就表示頁頭刷了頁尾沒刷,那肯定是刷到一半出錯了。
大小 | 說明 |
---|---|
4字節(jié) | 頁的校驗和Checksum |
4字節(jié) | 頁最后被修改時對應(yīng)的LSN的后4個字節(jié),正常情況下應(yīng)該與File Header里的FIL_PAGE_LSN的后4個字節(jié)相同。 |
3. 總結(jié)
頁是InnoDB存取數(shù)據(jù)的基本單位,默認(rèn)頁大小是16KB,InnoDB為了不同的目的設(shè)計了很多不同類型的頁,本文重點分析了存放用戶記錄的索引頁。頁的頭尾部分File Header和File Trailer是所有頁都有的一個通用結(jié)構(gòu),它們記錄了頁的一些通用狀態(tài)信息,和Checksum用來驗證頁的完整性。Page Header是索引頁特有的結(jié)構(gòu),它記錄了頁內(nèi)用戶記錄相關(guān)的狀態(tài)信息。User Records部分用來存放用戶記錄。另外,由于頁內(nèi)的記錄數(shù)量也不少,為了提高頁內(nèi)記錄的檢索效率,InnoDB在索引頁中加入了Page Directory,它通過將記錄分組,將組里最大的記錄的地址偏移量形成一個個槽,Page Directory就是由這些槽構(gòu)成的。檢索數(shù)據(jù)時,使用二分法快速定位到槽所在的組,就可以避免遍歷所有組的記錄了。
到此這篇關(guān)于MySQL索引頁結(jié)構(gòu)的文章就介紹到這了,更多相關(guān)MySQL索引頁結(jié)構(gòu)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MYSQL數(shù)據(jù)庫如何設(shè)置主從同步
大家好,本篇文章主要講的是MYSQL數(shù)據(jù)庫如何設(shè)置主從同步,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01SELECT INTO 和 INSERT INTO SELECT 兩種表復(fù)制語句簡單介紹
Insert是T-sql中常用語句,Insert INTO table(field1,field2,...) values(value1,value2,...)這種形式的在應(yīng)用程序開發(fā)中必不可少2012-11-11解決啟動MySQL服務(wù)時出現(xiàn)"mysql本地計算機(jī)上的MySQL服務(wù)啟動后停止"的問題
某一天我的MySQL啟動突然出現(xiàn)了異常:“mysql本地計算機(jī)上的MySQL服務(wù)啟動后停止,某些在未由其他服務(wù)或程序使用時將自動停止,”?,小編在網(wǎng)絡(luò)上面找了很多方法,MySQL啟動成功了,但是第二天開啟MySQL時還是出現(xiàn)了這個問題,現(xiàn)把兩種方法總結(jié)一下,需要的朋友可以參考下2023-11-11