文章目录
- 1. 前言
- 2. 数据页结构
- 3. 记录在页中的存储
- 4. 记录头信息
- 5. 页目录(Page Directory)
- 6. 页面头部(Page Header)
- 7.文件头部(Fiile Header)
- 8. 文件尾部(File Trailer)
不知不觉2022年已经结束了,没想到时间过得那么快,在这里祝大家新年快乐。
1. 前言
页是InnoDB管理存储空间的基本单位。
InnoDB为了不同目的实现了多种的不同类型的页,比如存储表空间头部的页、存放INODE信息的页等等。
但是我们比较关心的应该是存放表中记录的页,官方把这种存放记录的页称为索引页。
为了方便理解,下面把索引页称为数据页。
2. 数据页结构
数据页大小为16KB,这16KB被划分成多个部分。
数据页被分为7个部分,各个部分是干啥用的?请看下图
3. 记录在页中的存储
我们存储的记录是按照行格式存储在User Records部分。
但是在一开始生成页的时候,并没有User Records部分,当插入一条记录的时候,会从Free Space部分申请一个记录大小的空间。
当Free Space空间完全被User Records部分代替完之后,就意味着这个页用完了,如果再插入一条数据,就需要申请新的页了。
4. 记录头信息
在【MySQL】InnoDB记录存储结构有详细说到记录有信息。
但是这里主要说的是记录头的信息,先建一个表
CREATE TABLE page_demo(
c1 INT PRIMARY KEY,
c2 INT,
c3 VARCHAR(10000),
)CHARSET=ASCII ROW_FORMAT=COMPACT;
这是设置c1为主键,那么InnoDB就没有必要创建row_id列了。
并且指定了ASCII字符集以及COMPACT的行格式。
这一节主要讲述记录头信息的作用,因此可以简化一下行格式。
往表中插入数据
mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd');
Query OK, 4 rows affected (0.00 sec)
User Records部分的存储结构如下图所示
但是需要注意的是,这里把记录中的头信息和实际数据都用是十进制表示出来了,但是实际上是二进制位。
可以看见记录头有很多属性,那么各个属性代表什么呢?
delete_flag
:这个属性是标记当前记录是否被删除,占用1比特。- 值为0表示没有被删除,值为1表示记录被删除
- 为什么被删除的记录还在页中呢?这是因为如果真实移除这些记录,需要在磁盘上重新排列其他记录,这样会带来型号消耗。
- 所有被删除的记录会组成一个垃圾链表,记录这个链表占用的空间称为可重用空间。之后如果有新记录插入到表中,它们就可以覆盖被删除的这些记录占用的存储空间。
min_rec_flag
:B+树每层非叶子节点中的最小的目录项都会添加该标记。n_owned
:记录了该组有几条记录heap_no
:一条记录在堆中的相对位置- 记录在User Records中是亲密无间地排列的,这种结构被称为堆。
- 在页面前面的记录
heap_no
比较小,在页面后面的记录比较大。 - 例如,前面插入的4条数据中,
heap_no
的值分别为2、3、4、5 - 而0和1则是最小记录(
Infimun记录
)和最大记录(Supremum记录
)的heap_no
值,它们被称为虚拟记录或伪记录。Infimun记录
和Supremum记录
这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定单词组成的。由于其heap_no最小,所以这两个记录放在堆的最前面。 - 这里说的记录的大小是指主键的大小。
- 并且,
heap_no
是固定不变的,也就是值分配之后就不会发生改动了,即使被删除,值也不会发生变化。
record_type
:这个属性表示当前记录类型。- 一共有4个类型,0表示普通记录,1表示B+树非叶子节点的目录项记录,2表示Infimum记录,3表示Supermum记录。
next_record
:这个属性非常重要,它表示当前记录的真实数据到下一条记录的真实数据的距离。- 正数表示下一条记录在当前记录后面,负数表示下一条记录在当前记录前面。
- 下一条记录并不是指插入顺序中的下一条记录,而是按照主键值由小到大的顺序排列的下一条记录。
- 并且规定了Infimum记录的下一条记录就是本页中主键值最小的用户记录,本页主键值最大的记录的下一条记录就是Supremum记录。
- 如果将第二条数据删掉,那么第二条记录的
delete_flag
就会被设置为1,并第二条记录的next_record
变为0,第一条记录的next_record
指向第三天记录 next_record
的位置之所以在记录头和真实数据之间,**是因为这种向左读取就是记录头信息,向右读取就是真实数据。**前面说到变长字段长度列表和NULL值列表都是逆序存放的,这样可以使得记录中位置靠前的字段和它对应的字段长度信息在内存中距离更近,可以提高高速缓存命中率。- 如果将刚才删除的数据再次插入表中,会直接复用原来删除记录的空间。
5. 页目录(Page Directory)
如果想要查找某一条记录,并不会在链表中一个个遍历查找。
在设计InnoDB的时候,为我们的记录制作了一个类似图书目录的目录。
- 将所有正常的记录(包括Infimum和Supremum记录,但是不包括被删除的记录)划分为几个组
- 每个组的最后一条记录相当于“带头大哥”,其他记录相当于“小弟”,在“大哥”的
n_owned
记录该组有多少条记录。 - 将最后一条记录的地址偏移量单独拿出来,按顺序存储在靠近页尾的地方,这个地方就是页目录。页目录的这些地址偏移量称为槽,每个槽占用2字节。
比如page_demo表中有6条数据,就会被分成2组。
- 注意最小和最大记录的头信息中的n_owned属性 最小记录的n_owned值为1,这就代表着以最小记录结尾的这个分组中只有1条记录,也就是最小记录本身。 最大记录的n_owned值为5,这就代表着以最大记录结尾的这个分组中只有5条记录,包括最大记录本身还有我们自己插入的4条记录。
- 对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条件之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
- 所以 最小记录的 n_owned 是 1, 当前最大记录的 n_owned 是 5
那分组是这么分的呢?
- 在初始情况下,一个数据页中只有Infmum记录和Supremum记录这两条,它们分属于两个分组。页目录中也只有两个槽,分别代表Infmum记录和Supremum记录在页面中的地址偏移量。
- 之后每插入一条记录,都会从页目录中找到对应记录的主键值比待插入记录的主键值大并且差值最小的槽(从本质上来说,槽是一个组内最大的那条记录在页面中的地址偏移量,通过槽可以快速找到对应的记录的主键值),然后把该槽对应的记录的n_owned值加 1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分成两个组,其中一个组中 4条记录,另一个 5 条记录。这个拆分过程会在页目录中新增一个槽录这个新增分组中最大的那条记录的偏移量。
例如,再向表插入12条记录
怎么查找记录呢?
初始情况下最低的槽就是low = 0, 最高的槽就是hight=4,比如要找主键为6的记录。
- 计算中间槽的位置:(0+4)/2等于2,查找槽2的对应的记录的主键值为8,由于8>6,所以设置hight=2,
- 重新计算中间槽位置:(0 + 2)/2等于1,查找槽1的队友的记录的主键值为4,由于4<6,所以设置low=1,hight不变。
- 因为hight - low的值为1,所以确定主键值为6的记录在槽2。
- 因此可以从槽2的最小记录开始遍历查找。
- 槽2对应的记录是改组的主键值最大的记录,那么怎么定位槽2的最小记录呢?很简单,由于槽之间是挨着的,所以很容易找到槽1的最大记录,而槽1的最大记录的下一条记录就是槽2的最小记录。
6. 页面头部(Page Header)
页面头部是存储书局页中记录的状态信息,比如数据页存储了多少条记录、Free Space在页面中的地址偏移量、页目录中存储了多少槽等。
- PAGE_DIRECTION:假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是 PAGE_DIRECTION。
- PAGE_N_DIRECTION:假设连续几次插入新记录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION状态表示当然,如果最后一条记录的插入方向发生了改变,这个状态的值会被清零后重新统计。
7.文件头部(Fiile Header)
文件头部描述了一些通用各个页的信息,比如页的编号,它的上下页是谁等等。
文件头部由固定38个字节组成。
FIL_PAGE_SPACE_OR_CHKSUM
:这个属性代表当前页面的校验和 (checksum)。啥是校验和?就是对于一个很长的字节串来说,我们会通过某种算法计算出一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。这样在比较两个很长的字节串之前,先比较这两个长字节串的校验和。如果校验和都不一样,则两个长字节串肯定是不同的,这样就省去了直接比较两个长字节串的时间损耗。FILE_PAGE_TYPE
:页的类型
并且页是一个双向链表。
8. 文件尾部(File Trailer)
文件尾部是用来检测一个页是否完整,防止刷新的时有没有发生只刷新一部分的情况。
文件尾部由8字节组成,分为两个部分:
- 前4字节代表页的校验和。这个部分与 File Header 中的校验和相对应。每当一个页面在内存中发生修改时,在刷新之前就要把页面的校验和算出来。因为 File Header 在页面的前边,所以 File Header 中的校验和会被首先刷新到磁盘,当完全写完后,校验和也会被写到页的尾部。如果页面刷新成功,则页首和页尾的校验和应该是一致的。如果刷新了一部分后断电了,那么 File Header 中的校验和就代表着已经修改过的页,而File Trailer 中的校验和代表着原先的页,二者不同则意味着刷新期间发生了错误
- 后4字节代表页面被最后修改时对应的LSN的后4字节,正常情况下应该与 FileHeader 部分的FIL PAGE LSN的后4字节相同。
参考:
- 《MySQL是怎样运行的:从根儿上理解 MySQL》
- 【MySQL进阶】深入理解InnoDB数据页结构_小颜-的博客-CSDN博客