
衆所周知,MySQL的InnoDB存儲引擎中記憶體與硬碟互動的基本機關是頁。具體地,有資料頁(又稱為索引頁)、Undo頁、系統頁、溢出頁等類型。而所謂資料頁,即是用來存放資料記錄
概述
資料頁包含以下七個部分。如下圖所示,未标明所占空間大小的部分表示其所占空間不固定。其中Infimum、Supremum部分所占空間與該資料頁所使用的raw format行格式有關(例如在compact行格式下,其占用 2x(5+8)=26 個位元組)。其中對于File Header、File Trailer部分而言,是各類型頁通用的部分
描述目前頁通用的狀态資訊
「Page Header」描述資料頁特有的狀态資訊
「Infimum、Supremum」InnoDB插入的兩條虛拟記錄——即Infimum最小記錄、Supremum最大記錄
「User Records」存儲使用者插入的記錄資料,即使用者記錄
「Free Space」剩餘空間
「Page Directory」Page Directory頁目錄中包含若幹個槽,每個槽中會存儲某個資料記錄在該頁的位址偏移量
「File Trailer」用于檢驗目前頁的完整性
File Header 檔案頭
File Header檔案頭部,該部分固定使用38個位元組。如前所述,該部分在各類型頁中是通用的。故其隻是用于描述目前頁的一些基本狀态資訊,而不涉及資料頁這一類型下的相關資訊。下面對File Header中的各屬性依次做相關解釋、說明
- FIL_PAGE_SPACE_OR_CHKSUM
目前頁面的校驗和(Checksum),其占用4個位元組
- FIL_PAGE_OFFSET
目前頁的頁号,其占用4個位元組。InnoDB存儲引擎通過頁号即可找到該頁面。具體地,頁号從0開始編号,将頁号乘上資料頁的大小(對于非壓縮的頁,MySQL中預設大小為16KB)即可得到該頁的位址偏移量
- FIL_PAGE_PREV
前一個資料頁的頁号,其占用4個位元組
- FIL_PAGE_NEXT
後一個資料頁的頁号,其占用4個位元組。可以看到通過FIL_PAGE_PREV、FIL_PAGE_NEXT屬性,各資料頁之間實際上形成了一個
「雙向連結清單」。值得一提的是,并不是所有類型的頁都使用FIL_PAGE_PREV、FIL_PAGE_NEXT這兩個屬性。主要是資料頁類型(FIL_PAGE_INDEX)的頁使用該字段
- FIL_PAGE_LSN
目前頁最後一次修改時對應的日志序列位置(Log Sequence Number),其占用8個位元組
- FIL_PAGE_TYPE
目前頁的類型,其占用2個位元組。常見的有
- 「0x0002」 : FIL_PAGE_UNDO_LOG(Undo日志頁)
- 「0x0003」 : FIL_PAGE_INODE(段資訊節點)
- 「0x0004」 : FIL_PAGE_IBUF_FREE_LIST(Insert Buffer空閑清單)
- 「0x0005」 : FIL_PAGE_IBUF_BITMAP(Insert Buffer位圖)
- 「0x0006」 : FIL_PAGE_TYPE_SYS(系統頁)
- 「0x0007」 : FIL_PAGE_TYPE_TRX_SYS(事務系統頁)
- 「0x0008」 : FIL_PAGE_TYPE_FSP_HDR(表空間頭部資訊)
- 「0x0009」 : FIL_PAGE_TYPE_XDES(拓展描述頁)
- 「0x000A」 : FIL_PAGE_TYPE_BLOB(溢出頁)
- 「0x45BF」 : FIL_PAGE_INDEX(索引頁,即資料頁)
- FIL_PAGE_FILE_FLUSH_LSN
僅在系統表空間的一個頁中定義,代表檔案至少被重新整理到了對應的LSN值,其占用8個位元組
- FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
目前頁所屬的表空間,其占用4個位元組
Page Header 頁面頭
前面我們介紹了描述頁通用資訊的File Header部分,這裡我們來看看Page Header部分,其描述的就是資料頁類型的狀态資訊。下面對Page Header中的各屬性依次做具體解釋、說明
- PAGE_N_DIR_SLOTS
描述Page Directory頁目錄中槽的數量,其占用2個位元組。對于一個剛剛建立的空資料頁而言,其初值為2即有2個槽,分别指向Infimum最小記錄、Supremum最大記錄
- PAGE_HEAP_TOP
Free Space剩餘空間的起始位址,其占用2個位元組
- PAGE_N_HEAP
該資料頁中的所有記錄數量(含Infimum最小記錄、Supremum最大記錄、被删除的記錄),其占用2個位元組
- PAGE_FREE
已被删除的記錄的連結清單(即所謂的垃圾連結清單)的位址,其占用2個位元組
- PAGE_GARBAGE
已被删除的記錄所占用的總位元組數,其占用2個位元組。該部分空間可被重用
- PAGE_LAST_INSERT
最後一次記錄插入的位置,其占用2個位元組
- PAGE_DIRECTION
最後一次記錄插入的方向,其占用2個位元組。具體地,若本次插入的記錄的主鍵值比上次插入記錄的主鍵值大,則記錄插入的方向是右邊;反之為左邊
- PAGE_N_DIRECTION
相同方向連續插入的記錄數量,其占用2個位元組。若本次記錄插入的方向與之前的方向相反,則該值将被清零并重新計數
- PAGE_N_RECS
該資料頁中的有效記錄的數量(不含Infimum最小記錄、Supremum最大記錄、被删除的記錄),其占用2個位元組
- PAGE_MAX_TRX_ID
修改目前頁的最大事務ID,該值僅在二級索引中定義。其占用8個位元組
- PAGE_LEVEL
目前頁在B+樹中所處的層級,其占用2個位元組
- PAGE_INDEX_ID
索引ID,表示目前頁屬于哪個索引,其占用8個位元組
- PAGE_BTR_SEG_LEAF
B+樹葉子段的頭部資訊,僅在B+樹的Root頁定義,其占用10個位元組
- PAGE_BTR_SEG_TOP
B+樹非葉子段的頭部資訊,僅在B+樹的Root頁定義,其占用10個位元組
Infimum、Supremum 最小、最大記錄
所謂Infimum、Supremum部分,其實很簡單。其是InnoDB存儲引擎自動向資料頁插入的兩條記錄——Infimum最小記錄、Supremum最大記錄。由于這兩條記錄不是使用者插入添加的,故通常其又被稱作為
「僞記錄(虛拟記錄)」對于Infimum最小記錄而言,其記錄的資料内容部分固定為
「0x69 0x6E 0x66 0x69 0x6D 0x75 0x6D 0x00」;類似地,對于Supremum最大記錄而言,其記錄的資料内容部分固定為
「0x73 0x75 0x70 0x72 0x65 0x6D 0x75 0x6D」。聰明的朋友可能已經看出來了。實際上,上述兩條僞記錄的資料内容就是其記錄名稱(infimum、supremum)的ascii碼值
而Infimum最小記錄、Supremum最大記錄的記錄頭部分則取決于該資料頁所使用的raw format行格式
User Records 使用者記錄
該部分不言而喻相信大家都很清楚其作用,即是用來存儲使用者插入的資料記錄的。這裡我們以
「compact行格式」的資料記錄為例來展開介紹下
next_record字段
如我們之前所說的,在compact行格式下記錄頭資訊的next_record字段指的是下一條記錄的相對位置(位址偏移量)。但需要注意的是,其并不是指向下一條記錄的起始部分,而是指向下一條記錄的資料内容的起始部分。示意圖如下所示
這其實也解釋了為什麼記錄的額外資訊部分(變長字段的長度清單、NULL值标志位)是按照列的順序逆序排列的。因為此時資料内容部分中位置靠前的字段與其所對應的長度資訊的相對距離更近。根據
「局部性原理」可知,此舉将可能會提高CPU高速緩存的命中率
比較記錄的大小
聰明的朋友可能已經看出來了,User Records裡的記錄資料通過
「next_record字段」本質上構成了一個
「單向連結清單」。那麼問題來了?這個連結清單的順序是按照記錄插入的順序麼?答案:不是。
「實際上對于記錄來說,互相之間也是可以比較大小的。「具體地,對于一條」完整的記錄」而言,比較記錄的大小就是比較
「主鍵」的大小
例如我們依次順序地插入記錄1、記錄2、記錄3,其三條記錄的主鍵依次為14、47、35。從下圖可以看出,各記錄next_record字段指向的是下一個比它大的記錄,而非所謂的記錄插入順序。當其中記錄發生變化(新增、删除、修改)時,該連結清單也會适時調整,以滿足連結清單按記錄從小到大的排序規則
特别的,針對這個記錄連結清單而言無論其怎麼變化,其表頭、表尾永遠是固定不變的,分别是Infimum最小記錄、Supremum最大記錄,這也是此兩條僞記錄的命名來源。可以看出這兩條記錄相當于是記錄連結清單的哨兵節點
heap_no字段
該字段表示的是記錄在本頁中的位置。由于Infimum最小記錄、Supremum最大記錄在使用者插入的記錄的前面,故分别為0、1。故對于使用者記錄而言,該值從2開始。同樣以上圖為例,記錄1、記錄2、記錄3中該字段的值則分别為2、3、4
delete_mask字段
該字段為記錄删除的标志位。當我們删除某記錄時,不是直接從硬碟中删除,而是分為兩個階段
- 「delete mark階段」 :将記錄的該字段置為1
- 「purge階段」 :将該記錄加入所謂的 「垃圾連結清單」
對于垃圾連結清單中記錄所占用的空間即為所謂的
「可重用空間」。這樣下次當有新的記錄添加進來時,即可通過覆寫的方式來複用這部分存儲空間。當然,所謂的
「垃圾連結清單」也是通過被删除記錄的next_record字段作為指針來連結形成的
Free Space 剩餘空間
該部分即為頁面的剩餘空間。具體地,User Records部分從上往下使用剩餘空間,而Page Directory則從下往上使用剩餘空間。示意圖如下所示
Page Directory 頁目錄
前面我們說到資料頁中的各記錄實際上相當于一個單向連結清單,其中,最小記錄、最大記錄分别為表頭、表尾。而連結清單的查找效率非常低,每次都需從表頭開始進行周遊。為了提高查找效率,Page Directory頁目錄應運而生。首先将整個連結清單分為若幹個部分(即分組),然後将分組内最後一條記錄(即組内最大的記錄)的位址(即其在資料頁中的位址偏移量)存放在一個Slot槽中,各Slot槽根據其所指向的記錄按從大到小的順序在頁目錄中排列。示意圖如下所示。這裡為了簡便,各分組内的記錄沒有全部畫出來,而是隻是在圖中左側指明該分組中記錄的數量。其實關于分組中記錄的數量是存儲在該分組對應的Slot槽所指向的記錄(即分組内最後一條記錄,當然該記錄也是分組内最大的記錄)的
「n_owned」字段
這樣我們在該頁下如果需要根據
「主鍵」來查找某條記錄時,即可先利用Page Directory頁目錄中的各Slot槽,通過二分查找快速确定該記錄所在的分組,然後再按連結清單進行周遊。可以看到通過頁目錄大大縮小了連結清單周遊查找記錄時的範圍,提高了效率。這也是該部分為啥被稱之為目錄的緣由
具體地關于如何分組,基本步驟如下
- 資料頁初始化後,資料頁裡隻有最小記錄、最大記錄兩條記錄,它們分别屬于兩個分組
- 當插入一條新記錄到頁中後,其所在槽的确定方法是,從Slot 0槽(該槽所指向記錄顯然是各槽所指向的記錄中最小的)開始進行周遊,直至找到第一個 「槽所指向的記錄比該新記錄大」 的槽。随後将該槽所指向的記錄的 「n_owned字段」 值加1,即該分組中多了一條記錄
- 為了避免某個分組内記錄數量過多(因為這樣會導緻,在該分組内的查找周遊範圍較大),當分組内的記錄數達到8時,此時如果再向其中插入一條記錄,會導緻此分組拆分為兩個組,一個分組内4條記錄,另一個分組内5條記錄。當然增加了一個新分組,頁目錄中的槽資料也需要适時調整、維護,以保證頁目錄的有序、準确
前面我們提到,為了切實保證基于頁目錄的二分查找能夠真正達到縮小連結清單周遊範圍這一目的。我們需要對各分組下的記錄數量做限制,而在InnoDB引擎中,具體規定如下
- 最小記錄所在的分組隻能有1條記錄,即隻有它自己
- 最大記錄所在的分組的記錄數量隻能在1~8條記錄之間
- 其他分組的記錄數量隻能在是4~8條記錄之間
File Trailer 檔案尾
該部分與File Header檔案頭一樣,為各類型頁所通用。其目的用于檢驗目前頁的完整性。具體地其占用8個位元組,前4個位元組為校驗和(checksum),後4個位元組為頁面被最後修改時相應的日志序列位置(LSN)
這裡就基于校驗和的完整性校驗原理簡單的介紹下。其實也很簡單。在頁從記憶體同步回硬碟之前,先計算好校驗和(checksum),并賦給頁的File Header檔案頭、File Trailer檔案尾的校驗和字段。在頁從記憶體同步回硬碟後,如果該頁從頭到尾都被成功正确寫入磁盤的話,則硬碟上該頁的File Header檔案頭、File Trailer檔案尾的兩個校驗和資料應該是一緻的;反之,如果發現硬碟中該頁的File Header檔案頭、File Trailer檔案尾的兩個校驗和資料是不同的,則說明該頁同步過程中發生了意外(比如斷電)造成頁隻同步一部分到硬碟中了