文章目录
5.1 不同类型的页简介
InnoDB 为了不同的目的而设计了多种不同类型的页,比如存放表空间头部信息的页、存放Change Buffer信息的页、存放INODE信息的页,存放undo日志信息的页等等。那些存放表中记录的那种类型的页,官方称为索引页。鉴于上述还未介绍索引,目前先将存放记录的页称为数据页。
5.2 数据页结构快览
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 页面中的最小记录和最大记录 | 26字节 | 两个虚拟的记录i |
User Records | 用户记录 | 不确定 | 用户存储的记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Dictionary | 页目录 | 不确定 | 页中某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
5.3 记录在页中的存储
5.3.1 记录头信息的秘密
create table page_demo(
c1 int, c2 int , c3 varchar(10000), primary key(c1)
)charset=ascii row_format=compact;
5.4 Page Directory(页目录)
- 将所有正常的记录(包括Infimum和Supremum记录,但不包括已经移除到垃圾链表的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)相当于“带头大哥”,组内其余的记录相当于“小弟”。“带头大哥”记录的头信息中的n_owned属性表示该组内共有几条记录。
- 将每个组中最后一条记录在页面中的地址偏移量(就是该记录的真是数据与页面中第0个字节之间的距离)单独提取出来,按顺序存储到靠近页尾部的地方。这个地方就是Page Directory。页目录中的这些地址偏移量称为槽(Slot),每个槽占用2字节。页目录就说由多个槽组成的。
一个正常的页面也就是16KB大小,即16384字节,而2字节可用表示的地址偏移量范围是0~65535,所以用2字节表示一个槽足够了。
分组的记录数是有规定的:对于Infimum记录所在的分组只能有1条记录,也就是它自己,Supremum记录所在的分组拥有的记录条数只能在18条之间,剩下的分组中的记录条数范围只能是在48条之间。所以给记录进行分组是按照下面的步骤进行的:
- 在初始情况下,一个数据页中只有Infimum记录和Supremum记录这两条,他们分属于两个分组。页目录中也只有两个槽,分别代表Infimum记录和Supremum记录在页面中的地址偏移量。
- 之后每插入一条记录,都会从页目录中找到对应记录的主键值比待插入记录的主键值大并且差值最小的槽(从本质上来说,槽是一个组内最大的记录在页面中的地址偏移量,通过槽可以快速找到对应的记录的主键值),然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,知道该组中的记录数等于8个。
- 当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分成两个组,其中一个组中4条记录,另外一个5条记录。这个拆分过程会在页目录中新增一个槽,记录这个新增分组中最大的那条记录的偏移量。
在一个数据页中查找指定主键值的记录时,过程分为两步。
- 通过二分法确定该路所在分组对应的槽,然后找到该槽所在分组中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
5.5 Page Header(页面头部)
为了能得到存储在数据页中的记录的状态信息,比如数据页中已经存储了多少条记录,FreeSpace在页面中的地址偏移量、页目录中存储了多少个槽等,特意在数据页中定义了一个名为Page Header的部分,它是也结构的第二部分,占用固定的56字节,专门存储各种状态信息。
状态名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
PAGE_HEAP_TOP | 2字节 | 还未使用的空间的最小地址,也就是说从该地址之后就是free space |
PAGE_N_HEAP | 2字节 | 第一位表示本记录是否为紧凑型的记录,剩余的15为表示本页的堆中记录的数量 (包括Infimum,Supremum记录以及标记为“已删除”的记录) |
PAGE_FREE | 2字节 | 各个已删除的记录通过next_record 组成一个单向链表, 这个单向链表中的记录所占用的存储空间可以被重新利用;PAGE_FREE表示该链表头节点对应记录在页面中的偏移量 |
PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
PAGE_DIRECTION | 2字节 | 记录插入的方向 |
PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2字节 | 该页中用户记录的数量(不包括infimum和supremum记录以及被删除的记录) |
PAGE_MAX_TRX_ID | 8字节 | 修改当前页的最大事务id,该值仅在二级索引页面中定义 |
PAGE_LEVEL | 2字节 | 当前页在B+树中所处的层级 |
PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10字节 | B+树叶子节点段的头部信息,仅在B+树的根页面中定义 |
PAGE_BTR_SEG_TOP | 10字节 | B+树非叶子节点段的头部信息,仅在B+树的根页面中定义 |
- PAGE_DIRECTION:假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,繁殖则是左边。原来表示最后一条记录插入 方向的状态就是PAGE_DIRECTION。
- PAGE_N_DIRECTION:假设连续几次插入新记录的方向都是一致的,innodb会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION状态表示。
5.6 File Header(文件头部)
File Header通用于各种类型数据的页,也就是说各种类型的页都会以File Header作为第一个组成部分,它描述了一些通用于各种页的信息,比如这个页的编号是多少,它的上一页和下一页是谁,等等。该部分占用固定的38字节
状态名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 当MySQL的版本低于4.0.14时,该属性表示本页面所在的表空间ID;在之后的版本中,该属性表示页的校验和(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字节 | 页属于哪个表空间 |
- FIL_PAGE_SPACE_OR_CHKSUM:表示当前页面的校验和。什么是校验和?就是对于一个很长的字符串来说,我们会通过某种算法计算出一个比较短的值来代表这个很长的字符串,这个比较短的值就称为校验和。比较两个很长的字符串之前,先比较这两个长字符串的校验和,节省直接比较两个长字符串的时间损耗。
- FIL_PAGE_OFFSET: 每一个页都有一个单独的页号,如同我们的身份证号码一样。InnoDB通过页号来唯一定位一个页。
- FIL_PAGE_TYPE:表示当前页的类型。前文说过,InnoDB为了不同的目的而把页分为不同的类型,我们前面介绍的都是存储记录的数据页,其实还有很多其他类型的页:
类型名称 | 十六进制 | 描述 |
---|---|---|
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 | 索引页,也就是我们所说的数据页 |
用来存放记录的数据页的类型其实是FIL_PAGE_INDEX, 也就是索引页,至于啥是索引,且听下回分解。
前面在说记录的存储结构时,所说的溢出页的类型是FIL_PAGE_TYPE_BLOB,而存放正常记录的页面类型是FIL_PAGE_INDEX,两者是不一样的。
- FIL_PAGE_PREV、FIL_PAGE_NEXT: InnoDB可能无法一次性为大数据分配一个很大的存储空间,而如果分散到多个不连续的页中进行存储,则需要把这些页关联起来。这样通过建立一个双向链表就把许多的页串联起来了, 而无需这些页在物理上真正连着。值得注意的是, 并不是所有类型的页都有上一页与下一页的属性。
5.7 File Trailer (文件尾部)
我们知道,InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理。如果该页中的数据在内存中被修改了, 那么在修改后的某个时间还需要把数据刷新到磁盘中,但是如果在刷新还没有结束的时候断电了该咋办?为了检测一个页是否完整,InnoDB在每个页的尾部都加了一个File Trailer部分,这个部分由8字节组成,可以分成2个小部分。
- 前4字节代表页的校验和。这个部分与File Header中的校验和相对应。每当一个页面在内存中发生修改时,在刷新之前就要把页面的校验和算出来。因为FileHeader在页面的前边,所以File Header中的校验和会被首先刷新到磁盘,当完全写完后,校验和也会被写道页的尾部。如果页面书信成功,则页号和页尾的校验和应该是一致的。如果刷新了一部分后断电了,那么File Header中的校验和就代表着已经修改列过的页,而File Trailer中的校验和代表着原先的页,二者不同则意味着刷新期间发生了错误。
- 后4字节代表页面被最后修改时对应的LSN的后4字节,正常情况下应该与FileHeader部分FIL_PAGE_LSN的后4字节相同。这个部分也是用于校验页的完整性,不过我们目前还没说LSN是什么意思,所以大家可以先不用管这个属性。
File Trailer与File Header类似,都通用于所有类型的页。
5.8 总结
InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页称为数据页。一个数据页可以被大致分为7个部分,分别如下:
- File Header:表示页的一些通用信息,占固定的38字节。
- Page Header:表示数据页专有的一些信息,占固定的56字节
- Infimum + Supremum:两个虚拟的伪记录,分别表示页中的最小记录和最大记录,占固定的26字节
- User Records:真正存储我们插入的记录,大小不固定
- Free Space:页中尚未使用的部分,大小不固定
- Page Directory:页中某些记录的相对位置,也就是各个槽对应的记录在页面中的地址偏移量;大小不固定,插入的记录越多,这个部分占用的空间就越多。
- File Trailer:用于检验页是否完整,占固定的8字节
每个记录的头信息中都有一个next_record属性,从而可以使页中的所有记录串联成一个单向链表。
InnoDB会把页中的记录划分为若干组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,一个槽占用2字节。在一个页中根据主键查找记录是非常快的,分为两步。
1. 通过二分法确定记录所在分组对应的槽,并找到该槽所在分组中主键值最小的那条记录。
2. 通过记录的next_record 属性遍历该槽所在组的各个记录
每个数据页的File Header部分都有上一个页和下一个页的编号,所以所有的数据页会组成一个双向链表。
在将页的内存刷新到磁盘时,为了保证页的完整性,页首和页尾都会存储页中数据的校验和,以及页面最后修改时对应的LSN值(页尾只会存储LSN值的后4字节)。如果页首和页尾的校验和以及LSN值校验不成功,就说明刷新期间出现了问题。