InnoDB索引页面解析
背景介绍
在生产环境中,InnoDB表数据被大量删除后,原本连续存储的数据页之间会留下空闲页,导致表文件不再紧凑。然而,这些碎片页是如何产生的?能否被新插入的数据复用?能否被其他表复用?
本章节将深入剖析 InnoDB 物理文件管理机制,从原理层面解答上述疑问。
页面
InnoDB的物理文件体系主要包括系统表空间文件(ibdata*)、用户表空间文件(*.ibd)、独立的undo表空间文件(默认为undo_*)、临时表空间文件(temp_*.ibt)、redo日志文件(#ib_redo*)以及Double write文件(默认为#ib_16384_*.dblwr)等。
在这些文件中,除了redo日志文件外,其余文件都是由InnoDB标准页面(Page)构成。页(Page)是 InnoDB 中管理存储空间的基本单位,通常情况下,一个页面大小为16KB,可以通过设置innodb_page_size参数,在初始化数据库时,将页面大小调整为4KB到64KB之间的任意大小。表中的数据以记录的形式存储于页中。当查询的数据不在缓冲池(Buffer Pool)中时,InnoDB 会将该数据所在的整个页加载至 Buffer Pool 中;同理,将 Buffer Pool 中的脏页刷入磁盘时,同样以页为单位进行刷盘操作。
在InnoDB存储引擎中,存在多种页面类型,各自承担着不同的功能角色。包括:
- 表空间首页(FIL_PAGE_TYPE_FSP_HDR):用于记录表空间的元信息。
- 用户数据存储页面(FIL_PAGE_INDEX):用于存放用户表的数据和主键索引。
- 回滚段页面(FIL_PAGE_UNDO_LOG):用于存储undo日志,支持事务的回滚。
- 段索引节点页面(FIL_PAGE_INODE):用于管理数据文件中的段信息。
- 区信息页面(FIL_PAGE_TYPE_XDES):用于维护区的元数据。
- Insert Buffer 位图页面(FIL_PAGE_IBUF_BITMAP):跟踪各页是否有待处理的Insert Buffer 条目。
上述页面类型各异,但它们都共享相同的页面结构,包括通用页头(Fil Header)和通用页尾(Fil Trailer),其中记录的详细信息如下所示:
| 结构 | 名称 | 含义 | 字节数 |
|---|---|---|---|
| 通用页头 | FIL_PAGE_SPACE_OR_CHKSUM | 校验值。 | 4 |
| FIL_PAGE_OFFSET | 页面编号。 | 4 | |
| FIL_PAGE_PREV | 前页编号。 | 4 | |
| FIL_PAGE_NEXT | 后页编号 | 4 | |
| FIL_PAGE_LSN | 最后修改的LSN。 | 8 | |
| FIL_PAGE_TYPE | 页面类型。 | 2 | |
| FIL_PAGE_FILE_FLUSH_LSN | 仅在系统表空间的第0页定义,表示文件至少被刷新到了对应的LSN值。 | 8 | |
| FIL_PAGE_SPACE_ID | 表空间编号。 | 4 | |
| 页面内容 | ... | ... | ... |
| 通用页尾 | FIL_PAGE_END_LSN_OLD_CHKSUM | 页尾校验值 (4)。 | 4 |
| LSN的低4字节。 | 4 |
我们将重点介绍与用户数据直接相关的FIL_PAGE_INDEX类型,该类型通常被称为索引页类型。
索引页特点
索引页具备以下结构特点:
- 快速定位数据:页目录保存每组最大记录偏移量,通过二分查找与链表遍历的两步法即可快速命中目标。
- 数据完整性与恢复:页面从磁盘读取的时候会检查一次数据页内容校验,确保崩溃后能按序恢复数据,保障数据不丢失且不被损坏。
- 空间管理优化:空闲空间链表有效重组碎片空间,最大化提升页内空间利用率。
- 顺序访问优化:逻辑相邻的记录被存储在同一页内,这种布局更适合顺序扫描操作,从而显著提高读取效率。
上述结构特点协同优化了 InnoDB 的查询性能,同时确保了数据的可靠性。
索引页格式
如下图所示,索引页包括七个部分:文件头部、页面头部、最大最小记录、用户记录、空闲空间、页目录、文件尾部。
| 名称 | 中文名 | 含义 |
|---|---|---|
| File Header | 文件头部 | 页的一些通用信息,例如当前页号。 |
| Page Header | 页面头部 | 数据页专有的一些信息,例如该页中记录的数量。 |
| Infimum + Supremum | 最小和最大记录 | 两个虚拟的行记录,分别代表页内最小/最大记录。 |
| User Records | 用户记录 | 实际存储的表数据。 |
| Free Space | 空闲空间 | 页中尚未使用的空间。 |
| Page Directory | 页目录 | 页中每个分组内的最大记录的相对页首的地址偏移量。 |
| File Trailer | 文件尾部 | 校验页是否完整。 |
- 文件头部(File Header)
表3 文件头 名称
描述
占用空间大小
FIL_PAGE_SPACE_OR_CHKSUM
页的校验和(checksum值)。
4字节
FIL_PAGE_OFFSET
页号。
4字节
FIL_PAGE_PREV
上一个页的页号。
4字节
FIL_PAGE_NEXT
下一个页的页号。
4字节
FIL_PAGE_LSN
页面最新一次修改对应的日志序列号(LSN)。
8字节
FIL_PAGE_TYPE
页面类型。
2字节
FIL_PAGE_FILE_FLUSH_LSN
仅在系统表空间的第0页定义,代表文件至少被刷新到了对应的LSN值。
8字节
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
页归属的表空间space_id。
4字节
以下介绍几个较为重要的部分:
- FIL_PAGE_OFFSET:该页的页号,表示该页面在数据文件中的逻辑偏移量。
- FIL_PAGE_PREV,FIL_PAGE_NEXT:分别保存前一页、后一页的页号,把物理位置不连续的页串联成B+树的同一层逻辑相邻的页。在表空间第0页上,这两个字段被重载为FIL_PAGE_SRV_VERSION 和 FIL_PAGE_SPACE_VERSION。
一方面,大表难以一次拿到连续空间,只能分片分配;另一方面,频繁的页分裂与回收又会让逻辑相邻的页在磁盘上彼此远离。这两个指针正是用来在逻辑层面保持“索引有序”的链表结构,使得查询可以在物理不连续的页间快速跳转。
- FIL_PAGE_LSN:当前数据页最新被修改的LSN,Redo Log幂等性依赖此字段。在进行崩溃恢复时,如果发现Redo Log的LSN小于等于该值,就不再应用该Redo Log。
- 页面头部(Page Header)
表4 页面头部 名称
占用空间
描述
PAGE_N_DIR_SLOTS
2字节
页面目录中的目录数量。一个新建的空数据页包含2个目录,它们分别指向最大记录和最小记录。其中,第一个目录永远指向最小记录,最后一个目录则永远指向最大记录。
PAGE_HEAP_TOP
2字节
还未使用的空间最小地址。大于这个地址的且小于数据目录的空间都是未分配的Free Space,可以被后续使用。
PAGE_N_HEAP
2字节
bit 15是紧凑格式标志位,剩余15位(bit 14-0)表示堆中记录数量。(包括最小最大记录以及标记为删除的记录)。
PAGE_FREE
2字节
第一个已经被Purge物理删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用)。
PAGE_GARBAGE
2字节
已删除记录占用的字节数。
PAGE_LAST_INSERT
2字节
最后插入记录的位置。
PAGE_DIRECTION
2字节
记录插入的方向。
PAGE_N_DIRECTION
2字节
当前方向连续插入的记录数量,方向改变后清零。
PAGE_N_RECS
2字节
该页中记录的数量,不包括最大和最小记录。与PAGE_N_HEAP不同,如果记录被标记为delete_marked,这个值就会递减。
PAGE_MAX_TRX_ID
8字节
修改当前页的最大事务ID,该值仅在二级索引中定义。
PAGE_LEVEL
2字节
当前页在B+树中所处的层级, 0即叶子节点。
PAGE_INDEX_ID
8字节
索引ID,表示当前页属于哪个索引。
PAGE_BTR_SEG_LEAF
10字节
B+树叶子段的头部信息,仅在B+树的Root页定义。
PAGE_BTR_SEG_TOP
10字节
B+树非叶子段的头部信息,仅在B+树的Root页定义。
-
最小最大记录(Infimum and Supremum Records)
这是两条伪记录,在DYNAMIC行格式下,均由5字节的记录头信息和8字节的固定含义组成。由于这两条记录并非用户数据,因此被单独存放在Infimum+Supremum部分。
根据TaurusDB规定,最小记录是该数据页逻辑上最小的记录,所有用户记录都大于它;最大记录是数据页中最大的记录,所有用户记录都小于它。这两条记录在数据页被创建时生成,且不能被删除。
图2 最小最大记录示意图
- 用户记录(User Records)
用户的所有记录均以行格式存储在此处。在页面初次生成时,这部分内容为空;每次插入记录时,都会从Free Space中划分相应空间至User Records。默认情况下,记录之间并无间隙,但若重用已删除记录的空间,则可能会产生空间碎片。
记录在页面上按照插入顺序排序,每个记录均包含一个指向下一条记录的指针(next_record),从而构成单向链表。用户可从数据页的最小记录开始遍历,直至最大记录。在聚簇索引中,记录按主键顺序存储。在二级索引中,记录按索引键+主键排序。
User Records包含所有正常记录及所有已删除的(标记为delete_marked)记录,其中已被Purge物理删除的记录已被移入PAGE_FREE链表,不再被正常访问。
图3 用户记录示意图
- 空闲空间(Free Space)
从PAGE_HEAP_TOP开始到最后一个数据目录之间的空间即为空闲空间,这些空间会被重置为0。当用户需要插入记录时,首先会尝试在被删除记录的空间中查找可用空间;若未找到合适的空间,则从这里进行分配。空间分配给记录后,需要递增PAGE_N_RECS和PAGE_N_HEAP的值。
- 页目录(Page Directory)
为了加速页内的查询效率,避免每次查询都对单链表进行遍历,TaurusDB把页内所有有效记录(含 Infimum/Supremum,不含已删除记录)划分成若干组进行管理。对于被标记删除,但尚未被Purge物理删除的记录,其仍保留在 Page Directory 中,并继续挂在 next_record 逻辑链表上,待后续统一清理。
- 每个组的最后一条记录(即组内最大记录)作为owner_rec,它的 n_owned 字段保存本组记录数。
- 将owner_rec的地址偏移量按索引顺序收集起来,形成页目录(Page Directory),存放在页尾,从高地址向低地址增长。
- 数据页尾部(File Trailer)
这个部分处于数据页最后的位置,只有8个字节。前4个字节代表页的校验和,后4个字节存储FIL_PAGE_LSN的低位四字节。