InnoDB所有數據文件(ibdata以及ibd)都由頁組成。頁在內核實現中稱為page或block,page偏向于指向物理頁面,block偏向于指向page被加載到內存后,用來管理page的內存結構。在未被壓縮情況下,一個頁的大小為UNIV_PAGE_SIZE(16384,16K)。不同用途的頁具有相同格式的文件頭和文件尾,其中記錄了頁面校驗值、頁面編號、表空間編號、LSN等通用信息。頁面的組織方式在《InnoDB物理文件結構》中已有介紹,本文將深入InnoDB頁面結構。
1 頁面類型
以MySQL-8.0.30為例,所有頁面類型如下:
類型 | 含義 |
---|---|
FIL_PAGE_INDEX | B+樹節點 |
FIL_PAGE_RTREE | R-樹節點,R-tree是專門用來表示空間數據類型 |
FIL_PAGE_SDI | SDI是冗余備份的表元數據信息,同樣以B+樹存儲,此為SDI頁面類型 |
FIL_PAGE_TYPE_UNUSED | 目前此類型暫未使用 |
FIL_PAGE_UNDO_LOG | 存儲undo log的回滾段頁面 |
FIL_PAGE_INODE | 用于管理數據文件中的segment,每個inode頁可以存儲FSP_SEG_INODES_PER_PAGE(默認為85)個記錄 |
FIL_PAGE_IBUF_FREE_LIST | change buffer的空閑鏈表,change buffer介紹詳見《Buffer Pool詳解》 |
FIL_PAGE_TYPE_ALLOCATED | 新分配的頁面 |
FIL_PAGE_IBUF_BITMAP | page_no為1、1+16384*N的頁面都是FIL_PAGE_IBUF_BITMAP類型,用于記錄其后16384個頁面change buffer的信息 |
FIL_PAGE_TYPE_SYS | 系統頁 |
FIL_PAGE_TYPE_FSP_HDR | page no 0/16384*N的頁面都是extent描述頁,page no 0還記錄了與該table space相關的信息(FSP HEADER),類型為FIL_PAGE_TYPE_FSP_HDR |
FIL_PAGE_TYPE_TRX_SYS | 事務系統數據 |
FIL_PAGE_TYPE_XDES | 除page no為0的頁外所有extent描述頁的類型,page no為16384*N |
FIL_PAGE_TYPE_BLOB | 解壓的BLOB頁面 |
FIL_PAGE_TYPE_ZBLOB | 第一個壓縮的BLOB頁面 |
FIL_PAGE_TYPE_ZBLOB2 | 后續壓縮的 BLOB 頁面 |
FIL_PAGE_TYPE_UNKNOWN | 在舊表空間中,FIL_PAGE_TYPE在刷盤時會臨時替換為該值 |
FIL_PAGE_COMPRESSED | 壓縮頁面 |
FIL_PAGE_ENCRYPTED | 加密頁面 |
FIL_PAGE_COMPRESSED_AND_ENCRYPTED | 壓縮和加密頁面 |
FIL_PAGE_ENCRYPTED_RTREE | 加密的 R-tree 頁面 |
FIL_PAGE_SDI_BLOB | 未壓縮的 SDI BLOB 頁面 |
FIL_PAGE_SDI_ZBLOB | 壓縮后的 SDI BLOB 頁面 |
FIL_PAGE_TYPE_LEGACY_DBLWR | 舊的double write buffer頁面 |
FIL_PAGE_TYPE_RSEG_ARRAY | 回滾段數組頁面 |
FIL_PAGE_TYPE_LOB_INDEX | 未壓縮 LOB 的索引頁 |
FIL_PAGE_TYPE_LOB_DATA | 未壓縮 LOB 的數據頁 |
FIL_PAGE_TYPE_LOB_FIRST | 未壓縮 LOB 的第一頁 |
FIL_PAGE_TYPE_ZLOB_FIRST | 壓縮 LOB 的第一頁 |
FIL_PAGE_TYPE_ZLOB_DATA | 壓縮 LOB 的數據頁 |
FIL_PAGE_TYPE_ZLOB_INDEX | 壓縮 LOB 的索引頁。 此頁面包含一個 z_index_entry_t 對象數組。 |
FIL_PAGE_TYPE_ZLOB_FRAG | 壓縮 LOB 的片段頁面 |
FIL_PAGE_TYPE_ZLOB_FRAG_ENTRY | 片段頁的索引頁(壓縮的 LOB) |
2 通用頁面結構
任何頁面都有統一的文件頭和文件尾結構,用于記錄頁面的checksum校驗、頁面類型,用于維護邏輯頁面鏈表的前后邏輯頁面編號等。詳細結構如下:
2.1 Fil Header
名稱 | 大小 | 內容 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | MySQL4.0之前為space id,之后為CHECKSUM |
FIL_PAGE_OFFSET | 4 | 頁碼,每個表空間從0開始計數。頁碼乘以頁面大小便是當前頁面在數據文件中的偏移 |
FIL_PAGE_PREV | 4 | 指向B+樹同一層的前一個頁面,第一個頁面的FIL_PAGE_PREV為FIL_NULL |
FIL_PAGE_NEXT | 4 | 指向B+樹同一層的下一個頁面,最后一個頁面的FIL_PAGE_NEXT為FIL_NULL |
FIL_PAGE_LSN | 8 | LSN是一個一直遞增的整型數字,表示事務寫入到日志的字節總量,可以唯一區分對數據頁的修改,此處記錄頁面最后一次修改的LSN |
FIL_PAGE_TYPE | 2 | 頁面的類型 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 兩種作用:1)在系統表空間的第一個頁中,記錄MySQL關閉時checkpoint到的點,即刷入磁盤的頁面LSN至少在該值之上;2)只在FIL_PAGE_COMPRESSED類型的數據頁中被用于記錄壓縮信息 |
FIL_PAGE_SPACE_ID | 4 | 索引頁所在的表空間的ID |
2.2 Fil Trailer
文件尾的作用是校驗文件是否損壞,在每個頁面的結尾的8個字節中,分別存儲了checksum和LSN的后四位,對應于Fil Header中FIL_PAGE_LSN的內容。
3 索引頁結構
InnoDB的用戶表數據存儲于FIL_PAGE_INDEX(通常稱為索引頁)類型的頁面中,是InnoDB最重要的頁面類型之一。下面介紹其頁面結構:
3.1 Record
為了更好地理解Page Header和Page Directory。先介紹InnoDB索引頁中記錄的排列方式。記錄的格式可以按如下格式理解。
變長字段長度 | NULL 標志位 | 記錄頭信息 | 系統列 | Field 1 | ... | Field N |
---|
其中記錄頭信息中包含了next_record、heap_no、delete_flag、n_owned等重要信息。下面依次介紹:
- next_record:所有記錄在頁面中從前往后插入。假設id為primary key,id為1,3,2的三條記錄依次插入同一頁面,那么在頁面中三條記錄的排列順序也為1,3,2。三條記錄通過next_record屬性相連,即id為1的記錄的next_record指向id為2的記錄,id為2的記錄next_record指向id為3的記錄。
- heap_no:記錄在頁面中的物理編號,上述id為1,3,2的三條記錄的heap_no依次為N,N+1,N+2。
- delete_flag:記錄被刪除后并不是直接在頁面中抹去,而是標記上delete_flag。這樣做的目的是為了多版本并發控制服務(MVCC)。頁面中被delete mark的記錄也會通過next_record串聯成鏈表,記錄在Page Header中。當記錄不再被MVCC需要時,會由purge線程從頁面中徹底抹去。
- n_owned:本屬性與Page Directory相關,在Page Directory小節中介紹。
每個索引頁為了方便對頁內記錄的訪問,都添加了兩條系統記錄,它們沒有變長字段列表和NULL值列表,只包含記錄頭信息和真實數據:infimum和supremum,分別代表虛擬的最小記錄和虛擬最大記錄。這兩條記錄固定在Page Header之后,分別是heap_no為0和heap_no為1的記錄。從虛擬記錄infimum開始一直通過next_record指針訪問到虛擬記錄supremum,可以按邏輯大小遍歷頁內的所有記錄。
3.2 Page Directory
InnoDB B+樹記錄查詢只能查詢到數據頁級別。假設一條記錄的大小50字節,一個16384字節的頁面可以存儲300條記錄左右,如果頁內查詢如果都通過next_record從infimum遍歷到supremum,那么查詢將非常低效。
Page Directory如其名,作為數據目錄,是用來加速頁內記錄查找的。其由一個個slot組成,每個slot由兩個字節組成,每個Slot指向一條記錄,Slot的值是記錄在頁面內的偏移。每個Slot管理4~8條記錄。被指向的記錄作為組長記錄,管理位于其之前的4-8條記錄。組長記錄的n_owned為其管理的記錄數量。
如圖所示,Rec5管理Rec1-Rec5,n_owned為5;Rec10管理Rec6-Rec10,n_owned為5;Rec11管理Rec11-Rec14,n_owned為4;Rec18管理Rec15-Rec18,n_owned為4。
一個page至少有兩個Slot,第一個slot指向infimum記錄,最后一個Slot指向supremum記錄。Slot分配是從頁面的最后倒數8個字節的Fil Trailer開始逆序分配的,所以嚴格意義上上圖的Slot1-Slot4應該逆序。
在查找時首先數據目錄上對Slot進行二分查找,定位到具體的slot后,然后在Slot內進行順序查找。
3.3 Free Space
這部分是介于用戶記錄和Page Directory之間的一塊連續的未被使用的內存。用戶記錄從前往后增長,Page Directory從后往前增長。在空間足夠時,會直接從這里分配內存,當空間不足時,會重新整理頁面內的記錄,將碎片空間進行合并,或者分裂頁面,具體行為取決于InnoDB各場景下的具體策略。在將空間分配給記錄后,會遞增PAGE_N_RECS和PAGE_N_HEAP的值。
3.4 Page Header
有了前三小節的介紹,下面對Page Header進行介紹。Page Header主要索引頁內的統計信息,包含14部分,如下所示:
名稱 | 大小 | 含義 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | Page directory中的Slot個數 |
PAGE_HEAP_TOP | 2 | 空余空間的起始地址在頁內的偏移,如有新數據將從此位置插入 |
PAGE_N_HEAP | 2 | 頁面內所有的記錄數:系統記錄(Infimum和Supremum記錄)、用戶記錄、標記被刪除的記錄。標記刪除記錄并不會減少此值。 |
PAGE_FREE | 2 | 指向被標記刪除的記錄鏈表的第一個記錄。通過此記錄可以訪問所有標記刪除的記錄 |
PAGE_GARBAGE | 2 | 被標記刪除的所有記錄占用的總字節數,即可回收的空間大小 |
PAGE_LAST_INSERT | 2 | 指向最近一次被插入記錄的偏移量 |
PAGE_DIRECTION | 2 | 最近一次記錄插入的方向(從左或從右插入),每次插入時與PAGE_LAST_INSERT的記錄進行比較,以確認插入方向 |
PAGE_N_DIRECTION | 2 | 當前以相同方向順序插入記錄的個數 |
PAGE_N_RECS | 2 | 頁面上有效的用戶記錄的個數(不包括最小和最大記錄以及被標記為刪除的記錄) |
PAGE_MAX_TRX_ID | 8 | 修改當前頁面的最大事務ID,主要用于輔助判斷二級索引記錄的可見性。 |
PAGE_LEVEL | 2 | 當前頁面在B+樹中所在的層數,葉子結點的值為0 |
PAGE_INDEX_ID | 8 | 索引ID,表示當前頁屬于哪個索引 |
PAGE_BTR_SEG_LEAF | 10 | 僅僅在B+樹root頁定義,葉子節點段在inode page中的位置 |
PAGE_BTR_SEG_TOP | 10 | 僅僅在B+樹root頁定義,非葉子節點段在inode page中的位置 |
4 總結
本文在《InnoDB物理文件結構》的基礎上進一步介紹了InnoDB的頁面結構。首先介紹了所有InnoDB的頁面類型,接著介紹了InnoDB頁面的通用頁面結構Fil Header和Fil Trailer,最后從Record、Page Directory、Free Space和Page Header四個方面介紹了InnoDB索引頁的結構。