# 不同类型的页简介

页是InnoDB引擎里存储空间的基本单位,一个页的大小一般是16KB。InnoDB为了不同的目的而设计了许多种不同类型的页。比如存放表空间头部信息的页、存放undo的页等等。本文主要介绍一下存放表中记录的页(记录就是一行数据。本文当中一条条行数据都会被称之为一条条记录)。官网称之为索引页

# 索引页结构

数据页被分为7个部分。

名称 中文名 占用空间 简单表述
File Header 文件头部 38Byte 页的一些通用信息
Page Header 页面头部 56Byte 数据页专有的一些信息
Infinum + Supremum 最小记录和最大记录 26Byte 两个虚拟的行记录
User Records 用户记录 不确定 实际的行记录内容
Free Space 空闲空间 不确定 页中尚未使用的空间
Page Directory 页面目录 不确定 也中的某些记录的相对位置
File Trailer 文件尾部 8Byte 校验页是否完整

# 记录在页中的存储

在页的7个组成部分里。我们自己存储的记录会按照指定的行格式存储到UserRecords里。没插入一条新纪录。都会从FreeSpace中申请一个记录大小的空间划分到UserRecords部分。当FreeSpace部分的空间全部被UserRecords部分替换掉以后。也就代表这个页满了。再插入就需要申请新的页了。插入数据过程如图所示 image.png

InnoDB是如何管理User Recods里 这一条条行记录的。先看一下记录行格式的记录头信息

# 记录头信息

CREATE TABLE page_demo(
 c1 INT,
 c2 INT,
 c3 VARCHAR(10000),
 PRIMARY KEY (c1)
 ) CHARSET=ascii ROW_FORMAT=Compact;

先创建一个表。且我们为这个表指定了 ascii 字符集以及 Compact 的行格式。而且我们把C1列指定为主键。那么在行格式里就是没有row_id隐藏列了。

所以这个表中的行格式就是这样的

image.png

名称 大小 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型, 0 表示普通记录, 1 表示B+树非叶节点记录, 2 表示最小记录, 3表示最大记录
next_record 16 表示下一条记录的相对位置

这里我们看一下上面我们建的那个表page_demo的行格式简介图

image.png

插入三条记录
INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'),(4, 400, 'dddd');

这些记录的示意图如下

image.png

  • delete_mask:这个属性标记着当年钱记录是否被删除。所以每次我们使用delete删除数据的时候并不是物理删除,而是逻辑删除。这些被删除的记录会组成一个所谓的垃圾链表。这个链表占用的空间被称之为可重用空间。之后如果有新纪录插入到表中。这个把这些删除的记录占用的存储空间覆盖掉。
  • heap_no: 这个属性表示当前记录在本 页 中的位置,从图中可以看出来,我们插入的4条记录在本 页 中的位置分别 是: 2 、 3 、 4 、 5 。为啥不是从1开始。InnoDB给每个页里面加了俩个伪记录,一个代表最小,一个代表最大。这俩条记录的构造十分简单。都是由5字节大小的 记录头信息 和8字节大小的一个固定 的部分组成的,如图所示

image.png 这俩条记录因为不是我们自己定义的。所以他们并不存放在UserRecords里。细心的同学往上看。瞅瞅和页结构呼应上没。哈哈哈哈哈哈哈 牛吧!!

  • record_type:表示当前记录类型。0 表示普通记录, 1 表示B+树非叶节点记录, 2 表示最小记录, 3 表示最大记录。
  • next_record:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。注意这里并不是我们插入的顺序的下一条记录,而是按照主键值又小到大的顺序的下一条记录。 而且规定 ***Infimum***记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 ***Supremum***记录(也就是最大记录) ,给大家来个图具体看一下这记录记录是怎么存的

image.png

# 页目录-Page Directory

现在我们知道了表中的记录在页中按照主键值由小到大顺序串联成一个链表。如果我们想要根据主键值查询相对应的元素应该如何查找呢。InnoDB为我们的记录制作了一个类似于字典的目录。

  1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
  2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
  3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的 Page Directory ,也就是 页目录 (此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为 槽 (英文名: Slot ),所以这个页面目录就是由 槽 组成的。让我们从逻辑上看一下记录和页目录的关系。

image.png 从图中可以看出最小记录的 n_owned 值为1,而最大记录的 n_owned 值为 5。这是为啥。InnoDB在每个分组中的记录条数是有规定的。 对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。 这里大家可以发挥想象一下插入数据的时候组是如何变化的。

在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
  2. 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

举个例子吧。假如上面我们往上面创建的表中插入16条记录的话。那现在实际数据页中有多少条?当然是18条了。别忘了还有最大最小两条记录呢!!不多说了。上个图吧。

image.png 现在我们想找主键为7的应该怎么找。二分嘛

  1. 计算中间槽的位置(0+4)/2=2。槽二中存储记录的主键值8(别问为啥是8上面说了。)8>7.
  2. 继续计算中间槽的位置(0+2)/2=1。槽一对应的主键值为4。又因为4<7。这个时候是不就能确定我们要找的记录在槽2里了。槽2对应的主键值为8。行记录里面只存了next_record记录下一条的地址。也没告诉我们上一条的地址呀。真笨啊。我们可以找槽1的呀 槽1对应的主键值是4.然后往下遍历被。完美。找到了。

# Page Header (页面头部)

InnoDB 为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第 一条记录的地址是什么,页目录中存储了多少个槽等等。固定占56个字节。专门存储各种信息。

名称 大小 描述
PAGE_N_DIR_SLOTS 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 8 字节 修改当前页的最大事务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_N_DIRECTION 假设连续几次插入新记录的方向都是一致的, InnoDB 会把沿着同一个方向插入记录的条数记下来,这个条 数就用 PAGE_N_DIRECTION 这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
  • PAGE_DIRECTION 假如新插入的一条记录的主键值比上一条记录的主键值大,这条记录的插入方向是右边,反之则是左边。

# File Header(文件头部)

上面主要是介绍索引页记录的各种状态信息。但是File Header是针对各种类型的页都通用的。不同类型的页都会以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 Sequence Number)
FIL_PAGE_TYPE 2 字节 该页的类型
FIL_PAGE_FILE_FLUSH_LSN 8 字节 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 字节 页属于哪个表空间

InnoDB是以页为单位存放数据的。但是这些页在物理上也就是在磁盘上并不是相连的。FIL_PAGE_PREV 和 FIL_PAGE_NEXT 就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了

# File Trailer

InnoDB存储引擎会把数据存放到磁盘上。但是磁盘速度太慢。需要以页为单位把数据加载到内存中处理。如果该页当中的数据在内存中被修改了。在修改后的时间需要把数据同步到磁盘上。如果同步过程中MySQL崩溃了怎么办。为了检测一个页是否完整。设计了File Trailer。这部分又8个字节组成。可以分为俩部分。

  • 前四个字节代表页的校验和 这个部分是和 File Header 中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为 File Header 在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。 如果不一致就代表同步中间出错误了。 你瞅瞅!!!和FileHeader 上面对应上了吧。优雅!太优雅了!!!
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)(以后会给大家介绍一下这部分的)

# 总结

本文主要是针对MySQL的索引页也就是数据页进行讲解。当然不同类型的页会有部分差异。

  1. InnoDB为了不同的目的设计了不同类型的页,我们把用于存放记录的页叫做数据页。
  2. 一个数据页可以被大致划分为7个部分
    1. File Header,表示页的一些通用信息,占固定的38字节
    2. Page Hearder,表示数据页专有的一些信息
    3. Infimum+SuperNum,俩个虚拟的伪纪录,分别表示页中的最小和最大记录,占固定的26个字节
    4. UserRecords,真实存储我们插入的记录的部分,大小不固定
    5. FreeSpace,页面中尚未使用的部分
    6. PageDirectory,页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定
    7. File Trailer,用于校验页是否完整的部分,占用固定的8个字节
  3. 每个记录的头信息都有一个next_record属性,使页中的所有记录串联成一个单链表
  4. InnoDb会为把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中。
  5. 每个数据页的 File Header 部分都有上一个和下一个页的编号,所以所有的数据页会组成一个 双链表
  6. 为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时