文章

InnoDB 数据页结构

InnoDB 数据页类型

页是 InnoDB 管理存储空间的基本单元,默认大小为 16KB。

注意:这 16KB 是连续的存储空间。

在 InnoDB 中有很多种类型的页,如下表所示:

页类型

说明

FIL_PAGE_TYPE 取值

FIL_PAGE_INDEX

索引页,存储 B+树索引节点(包括根节点、中间节点、叶子节点)

0x45BF

FIL_PAGE_TYPE_ALLOCATED

已分配页,已分配但未使用的页

0x0000

FIL_PAGE_UNDO_LOG

Undo 日志页,存储 undo log

0x0002

FIL_PAGE_INODE

INODE 页,管理表空间内数据页分配情况

0x0003

FIL_PAGE_IBUF_FREE_LIST

Insert Buffer 空闲列表页,记录可以被 Insert Buffer 使用的空闲页(简单管理)

0x0004

FIL_PAGE_IBUF_BITMAP

Insert Buffer 位图页,详细记录 Insert Buffer 的使用情况,包括哪些页被完全或部分使用(精细管理)

0x0005

FIL_PAGE_TYPE_SYS

系统页,存储 InnoDB 的一些系统元数据

0x0006

FIL_PAGE_TYPE_TRX_SYS

事务系统页,存储事务系统相关的信息,包括当前活动事务的状态等

0x0007

FIL_PAGE_TYPE_FSP_HDR

表空间头页,存储表空间的元数据,如页分配信息、空间使用情况等

0x0008

FIL_PAGE_TYPE_XDES

扩展描述页,管理区(Extent)的分配情况

0x0009

FIL_PAGE_TYPE_BLOB

BLOB 页,存储 BLOB 或 TEXT 等大字段的数据

0x000A

数据页

我们要重点掌握的是存储记录的页,官方称之为索引页(FIL_PAGE_INDEX),我们一般叫它数据页,其结构如下图所示:

下面详细介绍下其中几个组成部分:

File Header

File Header 用来记录页的一些通用信息,固定占用 38 个字节,由下面这些部分组成:

名称

大小(字节)

说明

FIL_PAGE_SPACE_OR_CHKSUM

4

页的校验和(checksum)

FIL_PAGE_OFFSET

4

页在表空间中的偏移量

FIL_PAGE_PREV

4

当前页的上一页

FIL_PAGE_NEXT

4

当前页的下一页

FIL_PAGE_LSN

8

页最后被修改时的日志序列位置(LSN,Log Sequence Number)

FIL_PAGE_TYPE

2

页类型

FIL_PAGE_FILE_FLUSH_LSN

8

只在系统表空间使用,表示页最后一次被刷到磁盘时的LSN值

FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

4

页所属的表空间

我们来了解下 File Header 中几个重要的属性:

FIL_PAGE_SPACE_OR_CHKSUM

这个值是当前页的校验和(checksum),checksum 是一种数据校验方法,用于检查数据在传输或存储过程中是否发生变化。

它的原理是:使用算法将大数据量的原始数据计算出一个小数据量的值,即 checksum,并与原始数据一起传输或存储。传输或存储结束时,使用原始数据重新计算 checksum 并跟传输过来的 checksum 或之前存储的 checksum 比较,以判断数据是否完整。

checksum 类似 hash,区别是 hash 更安全,因为 checksum 通常使用较简单的算法,而 hash 通常使用更复杂的算法。

因此,checksum 通常用于数据传输和存储等要求高效率的场景,hash 通常用于密码存储、数字签名等追求安全的场景。

FIL_PAGE_OFFSET

页在表空间中的偏移量,也叫页号,从 0 开始,依次递增,如:

1. 表空间的第一个页,FIL_PAGE_OFFSET 的值为 0

2. 表空间的第二个页,FIL_PAGE_OFFSET 的值为 1

3. 如果某个数据页是表空间中的第 1000 个页,那么这个页的 FIL_PAGE_OFFSET 字段的值应该是 999,这意味着该数据页位于表空间文件从起始位置偏移 999 * 16KB 字节的位置(页默认为 16KB 大小)

FIL_PAGE_TYPE

页类型,详见文章开头的表格。

FIL_PAGE_PREV 和 FIL_PAGE_NEXT

我们知道 InnoDB 索引是 B+ 树结构,B+ 树的叶子节点组成了双向链表结构,这个双向链表就是使用 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 实现的。

有些类型的页没有这两个属性

这个部分是数据页独有的,用来记录数据页的状态信息,固定占用 56 个字节,由下面这些部分组成:

名称

大小(字节)

说明

PAGE_N_DIR_SLOTS

2

在Page Directory中的槽数

PAGE_HEAP_TOP

2

存储未使用空间的起始位置,即后面就是Free Space

PAGE_N_HEAP

2

当前页中的记录数(包括Infimum记录、Supremum记录和标记为删除的记录)

PAGE_FREE

2

第一个标记为删除的记录地址(所有标记为删除的记录通过next_record组成一个单链表)

PAGE_GARBAGE

2

标记为删除的记录占用的字节数

PAGE_LAST_INSERT

2

最后插入记录的位置

PAGE_DIRECTION

2

最后插入记录的方向,取值有:PAGE_LEFT(0x01)、PAGE_RIGHT(0x02)、PAGE_SAME_REC(0x03)、PAGE_SAME_PAGE(0x04)、PAGE_NO_DIRECTION(0x05)

PAGE_N_DIRECTION

2

一个方向连续插入的记录数

PAGE_N_RECS

2

当前页中的记录数(不包括Infimum记录、Supremum记录和标记为删除的记录)

PAGE_MAX_TRX_ID

8

修改当前页的最大事务id(仅在二级索引中定义)

PAGE_LEVEL

2

当前页在索引树中的层级(0x00表示叶子节点,即叶子节点在第0层)

PAGE_INDEX_ID

8

当前页所属的索引id

PAGE_BTR_SEG_LEAF

10

B+树叶子节点所在段的segment header(仅在B+树的Root页中定义)

PAGE_BTR_SEG_TOP

10

B+树非叶子节点所在段的segment header(仅在B+树的Root页中定义)

PAGE_DIRECTION 和 PAGE_N_DIRECTION

假如新插入的一条记录的索引值比上一条记录的索引值要大,我们就说这条记录的插入方向是右边(PAGE_RIGHT),反之则是左边(PAGE_LEFT)。

如果连续几次插入新记录的方向都是一样的,就把这个次数存储到PAGE_N_DIRECTION,直到后续出现不一样的插入方向则清零重新计数。

Infimum Record + Supremum Record

在 InnoDB 中,每个数据页都有两个虚拟记录:Infimum Record 和 Supremum Record。

Infimum Record 是最小记录,比当前页中的任何记录都要小;Supremum Record 是最大记录,比当前页中的任何记录都要大;

这两个记录在页创建时就被创建出来,并且不能被删除。

下面是一个简化的抽象结构,表明了Infimum Record、Supremum Record和普通用户记录之间的关系:

Page Directory

InnoDB 数据页中的记录会分成多个组,一个组可以包含多条记录,组中最后一条记录的 n_owned 存储了该组的记录数。Page Directory(页目录) 存储了每组最后一条记录的偏移量,这些偏移量也被称为目录槽(Directory Slots),有了这些槽,就可以利用二分法来加速页内记录的查找。

InnoDB 对每个组的记录数有如下规定:

  1. Infimum记录所在组只能有 1 条记录,即最小记录的 n_owned 总是为 1

  2. Supremum 记录所在槽可以有 1 ~ 8 条记录,即最大记录的 n_owned 取值范围为 [1, 8]

  3. 其他组可以有 4 ~ 8 条记录,即其他用户记录 n_owned 取值范围为 [4, 8]

当插入或删除记录时,可能触发组的分裂或平衡。

记录分组的过程如下:

  1. 最初,数据页只有 2 条记录,会分成 2 个组:Infimum 记录所在组和 Supremum  记录所在组

  2. 之后,每插入一条记录,都会从页目录中找到合适的槽,再将记录添加到该槽所在组,并将槽对应记录的 n_owned 加 1

  3. 如果在一个 n_owned 等于 8 的组中再添加一条记录,就会创建一个新的组,将这 9 条数据分配到这两个组中,一个组存放 4 条记录,一个组存放 5 条记录,同时也会在页目录中创建一个新的槽

在数据页内查找某个索引值对应记录的过程如下:

  1. 使用二分法确定目标记录所在槽

  2. 通过记录的next_record遍历该槽

下面是一个页目录和页中记录的关系示例图:

File Trailer

为了校验页的完整性,InnoDB 在页最后设计了一个 File Trailer 部分,固定占用 8 个字节,分成 2 个小部分:

(1) 前 4 个字节存储:checksum

每当一个页在内存中被修改了,就会重新计算checksum,更新File Header 和 File Trailer 中存储的 checksum。

当需要把内存中的页同步到磁盘上时,首先把File Header同步过去,如果同步后面的数据失败了(如断电),则磁盘中对应页的 File Trailer 中的 checksum 还是由修改前的数据计算的,与 File Header 中的 checksum 不一致,说明页不完整,同步出现了问题。

(2) 后 4 个字节存储:最后被修改时的LSN

最后被修改时的LSN(Log Sequence Number)也用于校验页的完整性,在 File Header 中也存储了一份,同步完需要比较两个 LSN 是否一致。

License:  CC BY 4.0