5.1 不同类型的页简介
InnoDB为了不同的目的设计了许多不同类型的页。
5.2 数据页结构快览
| 名称 | 中文名 | 占用空间大小 | 简单描述 |
|---|---|---|---|
| File Header | 文件头部 | 38字节 | 页的一些通用信息 |
| Page Header | 页面头部 | 56字节 | 数据而专有的一些信息 |
| Infimum + Supremum | 最小最大记录 | 26字节 | 两个虚拟的行记录 |
| User Records | 用户记录 | 不确定 | 实际存在的行记录内容 |
| Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| Page Directory | 页面目录 | 不确定 | 页中某些记录的相对位置 |
| File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
5.3 记录在页中的存储
初始阶段,没有User Record这个部分,每当我们插入一条记录,都会从Free Space部分,申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了。
5.3.1 记录头信息的秘密
CREATE TABLE page_demo (
c1 INT,
c2 INT,
c3 VARCHAR ( 10000 ),
PRIMARY KEY ( c1 )
) CHARSET=ASCII ROW_FORMAT=COMPACT;
| 名称 | 大小(bit) | 描述 |
|---|---|---|
| 预留位1 | 1 | 没有使用 |
| 预留位2 | 1 | 没有使用 |
| delete_mask | 1 | 标记该记录是否被删除 |
| min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
| n_owned | 4 | 表示当前记录拥有的记录数 |
| heap_no | 13 | 表示当前记录在记录堆的位置信息 |
| record_type | 3 | 表示当前记录的类型 |
| next_record | 16 | 表示下一条记录的相对位置 |
插入数据
INSERT INTO page_demo VALUES
(1, 100, 'aaaa'),
(2, 200, 'bbbb'),
(3, 300, 'cccc'),
(4, 400, 'dddd');
-
delete_mask
标记该记录是否被删除。0:没有被删除,1:已经被删除
被删除的记录不会立即从磁盘上移除,因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个
垃圾链表,这个链表中的记录占用空间被称为可重用空间,之后如果有新记录插入到表中的话,可能把这些空间覆盖掉。 -
min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记。0:不是B+树的非叶子节点中的最小记录
-
n_ownd
表示当前记录拥有的记录数。
-
heap_no
-
表示当前记录在本
页的位置。 -
两条伪记录:最小记录和最大记录,heap_no分别为0和1,也就是说它们的位置最先前。
-
通过主键来比较大小
-
由于不是我们定义的记录,所以不存放在User Record,而是放在Infimum + Supremum部分
-
示意图如下
-
-
record_type
表示当前记录的类型。0:普通记录,1:B+树非叶节点记录,2:最小记录,3:最大记录
-
next_record
- 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
- 单向链表,通过一条记录找到它的下一条记录
- 按主键大小排序
- 第一条是
Infimum记录,最后一条是Supremum记录 - 删除记录相当于删除链表中的一个节点
删除一条数据
DELETE FROM page_demo WHERE c1 = 2;
可以看出:
- 第2条记录还在,只是delete_mask改成了1
- 第2条记录的next_record变成了0,意味着没有下一条记录了
- 第1条记录的next_record指向了第3条
- n_owned从5变成了4
再插入一条数据
INSERT INTO page_demo VALUES(2, 200, 'bbbb');
InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
5.4 Page Directory(页目录)
- 将所有正常的记录(包括最大和最小记录,但不包括标记为已删除的)划分为几个组
- 每个组的最后一条记录的头信息中的n_owned属性表示该组内共有几条记录
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到
页的尾部,这个地方就是所谓的 Page Directory。页面目录中的这些地址偏移量被称为为槽(Slot),所以这个页目录就是由槽组成的。
分组中的记录条数规定:最小记录所在的分组只能有1条记录,最大记录所在的分组能有1 ~ 8条记录,剩下的分组中记录条数的范围是4 ~ 8条之间。
再插入一些数据
INSERT INTO page_demo values
(5, 500, 'eeee'), (6, 600, 'ffff'), (7, 700, 'gggg'),
(8, 800, 'hhhh'), (9, 900, 'iiii'), (10, 1000, 'jjjj'),
(11, 1100, 'kkkk'), (12, 1200, 'llll'), (13, 1300, 'mmmm'),
(14, 1400, 'nnnn'), (15, 1500, 'oooo'), (16, 1600, 'pppp');
所以在一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录
- 通过记录的
next_record属性遍历该槽所在的组中的各个记录
5.5 Page Header(页面头部)
反映一个数据页中存储的记录的状态信息。
| 名称 | 占用空间(byte) | 描述 |
|---|---|---|
| PAGE_N_DIR_SLOT | 2 | 在页目录中的槽数量 |
| PAGE_HEAP_TOP | 2 | 还未使用的空间最小地址,也就是说从此处开始之后就是Free Space |
| PAGE_N_HEAP | 2 | 本页记录的数量(包括最小和最大记录以及标记为删除的记录) |
| PAGE_FREE | 2 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组合一个单链表,可以被重新利用) |
| PAGE_GARBAGE | 2 | 已删除记录占用的字节数 |
| PAGE_LAST_INSERT | 2 | 最后插入记录的位置 |
| PAGE_DIRECTION | 2 | 记录插入的方向 |
| PAGE_N_DIRECTION | 2 | 一个方向连接插入的记录数量 |
| PAGE_N_RECS | 2 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
| PAGE_MAX_TRX_ID | 2 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
| PAGE_LEVEL | 2 | 当前页在B+树中所处的层级 |
| PAGE_INDEX_ID | 8 | 索引ID,表示当前页属于哪个索引 |
| PAGE_BTR_SEG_LEAF | 10 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
| PAGE_BTR_SEG_TOP | 10 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
-
PAGE_DIRECTION
假如新插入的一条记录的主键值比上一条记录大,我们说这条记录的插入方向是右边,反之则是左边。
-
PAGE_N_DIRECTION
假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记录下来。一旦方向改变了,这个值就是清零。
5.6 File Header(文件头部)
File Header对各种类型的页通用,页会以File Header作为第一个组成部分。
| 名称 | 占用空间(byte) | 描述 |
|---|---|---|
| FIL_PAGE_SPACE_OR_CHKSUM | 4 | 页的校验和(checksum值),通过某种算法计算一个较短的值来代表很长的字符串 |
| FIL_PAGE_OFFSET | 4 | 页号,唯一的,InnoDB通过页号可以定位一个页 |
| FIL_PAGE_PREV | 4 | 上一个页的页号 |
| FIL_PAGE_NEXT | 4 | 下一个页的页号 |
| FIL_PAGE_LSN | 8 | 页面被最后修改时对应的日志序列位置 |
| FIL_PAGE_TYPE | 2 | 页的类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的 LSN 值 |
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 页属于的表空间 |
| 类型名称 | 十六进制 | 描述 |
|---|---|---|
| FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没使用 |
| FIL_PAGE_UNDO_LOG | 0x0002 | Undo日志页 |
| FIL_PAGE_INODE | 0x0003 | 段信息节点 |
| FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
| FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
| FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
| FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
| FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
| FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
| FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
| FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的数据页 |
-
FIL_PAGE_PREV和FIL_PAGE_NEXT
上一个和下一个页号,以此建立一个双向链表。
5.7 File Trailer(文件尾部)
8字节组成
- 前4字节代表页的校验和,和File Header中的校验和相对应。同步内存数据到磁盘时,会先同步File Header的校验和,完全写完时,File Trailer的校验和也会被写到页的尾部。如果完全同步成功,两个校验和应该是一致的。反之则意味着同步过程出了问题。
- 后4字节代表页面最后修改时对应的日志序列位置(LSN)
5.8 总结
- InnoDB为了不同的目的设计了不同类型的页。
- 一个数据页可以被大致分为7个部分
- 每个记录的头信息中都有一个
next_record属性,形成一个单链表。 - InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中。
- 每个数据页的File Header部分都有上一个和下一个页的编号,形成一个
双链表。 - 为保证从内存中同步到磁盘的页的完整性,页的尾部会进行二次校验。