不同类型的页
页是InnoDB管理存储的基本单位,一个页的大小一般是16KB。类型有:存放表空间头部信息的页、存放ChangeBuffer信息的页、存放INODE信息的页、存放undo日志信息的页。
2. 数据页结构
数据页代表的这块16KB大小的存储空间可以划分为多个部分。
![](https://i-blog.csdnimg.cn/blog_migrate/53ffe4dd74eec9d510dfafe592f6a896.png)
InnoDB数据页结构说明7分部分大致的
名称 | 中文名 | 占用空间 | 简单描述 |
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专用的一些信息 |
Infimum + Supremum | 页面中的最小记录和最大记录 | 26字节 | 两个虚拟的记录 |
User Records | 用户记录 | 不确定 | 用户存储的记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚为使用的空间 |
Page Directory | 页目录 | 不确定 | 页中某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验也是否完整 |
3. 记录在页中的存储
在一开始生成页面的时候,其实并没有User Records部分,每当插入一条记录时,都会从Free Space部分申请一个记录大小的空间,并将这空间划分到User Records部分。当Free Space部分的空间全部被User Records部分代替之后,也就以为这个页使用完了。
![](https://i-blog.csdnimg.cn/blog_migrate/d24b137d4c854b0f6b0b687923ee57af.jpeg)
3.1 记录头信息
记录头信息的属性及描述
![](https://i-blog.csdnimg.cn/blog_migrate/2cd416aa0ba10525fdf12b24471175da.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/52a3e9d68746aff4f12df0364690fc2e.png)
CREATE TABLE `page_demo` (
`c1` int(255) NOT NULL,
`c2` int(255) NULL,
`c3` varchar(10000) NULL,
PRIMARY KEY (`c1`)
) CHARACTER SET = ascii ROW_FORMAT = COMPACT;
INSERT INTO `test`.`page_demo`(`c1`, `c2`, `c3`) VALUES (1, 100, 'aaaa'),
(2,200,'bbbb'),(3,300,'cccc'),(4,400,'dddd');
这4条记录在InnoDB中的行格式如下(只展示记录头和真实数据),列中数据均用十进制表示:
![](https://i-blog.csdnimg.cn/blog_migrate/e760a4815ac5e6125e1705bd427172b7.jpeg)
delete_mask
这个属性标记当前记录是否被删除,值为1的时候表示记录被删除掉了,值为0的时候表示记录没有被删除。
可以看出,当删除一条记录时,只是标记删除,实际在页中还没有被移除。这样做的主要目的是,以后如果有新纪录插入表中,可以复用这些已删除记录的存储空间。
min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记,并设置为1,否则为0。
n_owned
表示当前记录拥有的记录数,页中的数据其实还会分为多个组,每个组会有一个最大的记录,最大记录的 n_owned 就记录了这个组中的记录数。在后面介绍 Page Directory 时会看到这个属性的用途。
heap_no
这个属性表示当前记录在本页中的位置。
record_type
记录类型:0 表示普通记录,1 表示B+树非叶子节点记录,2 表示最小记录,3 表示最大记录,1xx 表示保留
还是以前面索引结构图来看,上面两层的非叶子节点中的记录 record_type 都应该为 1。最底层的叶子节点应该就是普通记录,record_type 为 0。其实每个页还会有一个最小记录和最大记录,record_type 分别为 2 和 3,这个最小记录和最大记录其实就是后面要说的 Infimum 和 Supremum。
next_record
表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量,如果没有下一条记录就是 0,当为负数时,说明当前记录的下一条记录在当前记录的前面。
数据页中的记录看起来就像下图这样,按主键顺序排列后,heap_no 记录了当前记录在本页的位置,然后通过 next_record 连接起来。
Infimum记录是本页中主键最小的用户记录,Supremum记录本页中主键最大的记录。
![](https://i-blog.csdnimg.cn/blog_migrate/d36ce211452757812405bbc28f98c89b.png)
注意 next_record 指向的是记录头与数据之间的位置偏移量。这个位置向左读取就是记录头信息,向右读取就是真实数据,而且之前说过变长字段长度列表和NULL值列表中都是按列逆序存放的,这时往左读取的标识和往右读取的列就对应上了,提高了读取的效率。
如果删除了其中一条记录,delete_mask 就设置为 1,标记为已删除,next_record 就会设置为 0。其实页中被删除的记录会通过 next_record 形成一个垃圾链表,供以后插入记录时重用空间。
![](https://i-blog.csdnimg.cn/blog_migrate/0e166b2cb381d34de790d6872a3aecf2.png)
4. Page Directory(页目录)
Page Directory(页目录)大致的原理如下:
将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。怎么划分先不关注。
每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该组内共有几条记录。
将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页尾部的地方,这个地方就是所谓的Page Directory。
mysql规定对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1-8 条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间。比方说现在的page_demo表中正常的记录共有18条,InnoDB会把它们分成5组,第一组中只有一个最小记录,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/ba200c791963e90c49806374e050e17a.png)
通过Page Directory在一个数据页中查找指定主键值的记录的过程分为两步:
通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
通过记录的next_record属性遍历该槽所在的组中的各个记录。
对于链表的查询性能优化,思想上基本上都是通过二分法实现的。上面介绍的Page Directory,跳跃表和查找树都是如此。
5. Page Header
Page Header 用来记录数据页的状态信息,由14个部分组成,共占用56字节。
![](https://i-blog.csdnimg.cn/blog_migrate/7cfad67fd9af904d813824eef740ae42.png)
PAGE_N_DIR_SLOTS
页中的记录会按主键顺序分为多个组,每个组会对应到一个槽(Slot),PAGE_N_DIR_SLOTS 就记录了 Page Directory 中槽的数量。
PAGE_HEAP_TOP
PAGE_HEAP_TOP 记录了 Free Space 的地址,这样就可以快速从 Free Space 分配空间到 User Records 了。
PAGE_N_HEAP
本页中的记录的数量,包括最小记录(Infimum)和最大记录(Supremum)以及标记为删除(delete_mask=1)的记录。
PAGE_FREE
已删除的记录会通过 next_record连成一个单链表,这个单链表中的记录空间可以被重新利用,PAGE_FREE 指向第一个标记为删除的记录地址,就是单链表的头节点。
PAGE_GARBAGE
标记为已删除的记录占用的总字节数。
PAGE_N_RECS
本页中记录的数量,不包括最小记录和最大记录以及被标记为删除的记录,注意和 PAGE_N_HEAP 的区别。
6. File Header(文件头部)
页头部存的是一个数据页的概要信息,是一个页专有的,而文件头存的是各种页通用的信息,比如页的类型是什么、页的编号是多少、上一页的页号是多少、下一页的页号是多少等等。
状态名称 | 占用空间 | 描述 |
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum)值 |
FIL_PAGE_OFFSET | 4字节 | 页号 |
FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log SequenceNumber,日志序列号)值 |
FIL_PAGE_TYPE | 2字节 | 该页的类型 |
FIL_PAGE_FILE_FLUSH LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
FIL PAGE ARCH LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
既然有上一页、下一页的定义,说明页与页之间其实是互相连接的,它们之间就像一个双向链表(比如 Java 的 LinkedList)那样,如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/60e560ffbfc291f758db517ae902bc82.png)
类型名称 | 十六进制 | 描述 |
FIL PAGE TYPE ALLOCATED | 0x0000 | 最新分配,还未使用 |
FIL PAGE UNDO LOG | 0x0002 | undo日志页 |
FIL PAGE INODE | 0x0003 | 存储段的信息 |
FIL PAGE IBUF FREE LIST | 0x0004 | Change Buffer 空闲列表 |
FIL PAGE IBUF BITMAP | 0x0005 | Change 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 | 溢出页 |
FIL PAGE INDEX | 0x45BF | 索引页,也就是我们所说的数据页 |
7. File Trailer(文件尾部)
mysql中内存和磁盘的基本交互单位是页。如果内存中页被修改了,那么某个时刻一定会将内存页同步到磁盘中。如果在同步的过程中,系统出现问题,就可能导致磁盘中的页数据没能完全同步,也就是发生了脏页的情况。为了避免发生这种问题,mysql在每个页的尾部加上了File Trailer来校验页的完整性。File Trailer由8个字节组成:
前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。简单理解,就是在数据发生变更前,首先会计算出校验和,在File Header添加检验和,数据处理完成后,会在File Trailer添加校验和。如果File Header和File Trailer的校验和一致则表示数据页是完整的。否则,则表示数据页是脏页。
后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
主要参考资料:
掘金小册《MySQL 是怎样运行的:从根儿上理解 MySQL》