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索引页的结构。