文章目录
今天我们来说一下页,它是 InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB 。存放我们常用的表中记录的那种类型的页,官方称这种存放记录的页为
索引( INDEX )页,而这些表中的记录就是我们日常所称的
数据 ,所以目前还是叫这种存放记录(数据)的页为
数据页。InnoDB 都是以页为单位存放数据的。
数据页结构示意图
数据页代表的这块 16KB 大小的存储空间可以被划分为多个部分,不同部分有不同的功能。从图中可以看出,一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。
在页的7个组成部分中,我们自己存储的记录会按照我们指定的 行格式 存储到 User Records 部分。但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:
组成部分
File Header(38字节) 文件头部
页的一些通用信息。说明这个页的编号是多少,它的上一个页、下一个页都分别是谁,记录页的位置。
Page Header(56字节) 页面头部
数据页中存储记录的状态信息。本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽。
Infimum+supremum (26字节) 最小记录和最大记录
两个虚拟的行记录。
User Records 用户记录
实际存储的行记录内容。
表中记录的行格式示意图如下所示:
关于图中具体字段的含义解释如下:
- 预留位1:没有使用
- 预留位2:没有使用
- delete_mask 这个属性标记着当前记录是否被删除,占用1个二进制位,值为 0 的时候代表记录并没有被删除,为 1 的时 候代表记录被删除掉了。
- min_rec_mask B+树的每层非叶子节点中的最小记录都会添加该标记。
- n_owned 表示当前记录拥有的记录数。
- head_no 这个属性表示当前记录在本 页 中的位置
- record_type 这个属性表示当前记录的类型,一共有4种类型的记录, 0 表示普通记录, 1 表示B+树非叶节点记录, 2 表 示最小记录, 3 表示最大记录。
- next_record - 它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。- 下一条记录 指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)。
为了更形象的表示一下这个 next_record 起到的作用,我们用箭头来替代一下next_record 中的地址偏移量,从图片可以看出记录按照主键从小到大的顺序形成了一个单链表。最大记录 的 next_record 的值为 0 ,这也就是说最大记录是没有 下一条记录 了,它是这个单链表中的最后一个节点。
Free Space 空闲空间
页中尚未使用的空间
Page Directory 页目录
页中的某些记录的相对位置。
现在我们了解了记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:
SELECT*FROM page_demo WHERE c1 =3;
最笨的办法:从 Infimum 记录(最小记录)开始,沿着链表一直往后找,总有一天会找到,在找的时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了,因为该节点后边的节点的主键值依次递增。
我们来看一下一种比较简单的方法,引入目录的概念,也就是索引的由来,具体过程如下:
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的page Directory ,也就是页目录.页面目录中的这些地址的偏移量被称为槽(Slot),所以这个页面目录就是由槽组成的。
比方说现在的 page_demo 表中正常的记录共有6条, InnoDB 会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录,看下边的示意图:
从这个图中我们需要注意这么几点:
- 现在 页目录 部分中有两个槽,也就意味着我们的记录被分成了两个组,槽1中的值是112 ,代表最大记录的地址偏移量(就是从页面的0字节开始数,数112个字节); 槽0 中的值是 99 ,代表最小记录的地址偏移量。
- 注意最小和最大记录的头信息中的 n_owned 属性: - 最小记录的 n_owned 值为 1 ,这就代表着以最小记录结尾的这个分组中只有 1 条记录,也就是最小记录本身。- 最大记录的 n_owned 值为 5 ,这就代表着以最大记录结尾的这个分组中只有 5 条记录,包括最大记录本身还有我们自己插入的 4 条记录。
分组(槽)的依据
对于最小记录所在的分组只能有 1 条记录, 最大记录所在的分组拥有的记录条数只能在 18 条之间,剩下的分组中记录的条数范围只能在是 48 条之间。
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 之后每插入一条记录,都会从 页目录 中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的 n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在 页目录 中新增一个 槽 来记录这个新增分组中最大的那条记录的偏移量。
查找槽的位置
使用二分法快速查找
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。
实例如下:
4个槽的编号分别是: 0 、 1 、 2 、 3 、 4 ,所以初始情况下最低的槽就是 low=0 ,最高的槽就是high=4 。比方说我们想找主键值为 6 的记录,过程是这样的:
- 计算中间槽的位置: (0+4)/2=2 ,所以查看 槽2 对应记录的主键值为 8 ,又因为 8 > 6 ,所以设置high=2 , low 保持不变。
- 重新计算中间槽的位置: (0+2)/2=1 ,所以查看 槽1 对应的主键值为 4 ,又因为 4 < 6 ,所以设置low=1 , high 保持不变。
- 因为 high - low 的值为1,所以确定主键值为 5 的记录在 槽2 对应的组中。此刻我们需要找到 槽2 中主键值最小的那条记录,然后沿着单向链表遍历 槽2 中的记录。但是我们前边又说过,每个槽对应的记录都是该组中主键值最大的记录,这里 槽2 对应的记录是主键值为 8 的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿到 槽1 对应的记录(主键值为 4 ),该条记录的下一条记录就是 槽2 中主键值最小的记录,该记录的主键值为 5 。所以我们可以从这条主键值为 5 的记录出发,遍历 槽 2 中的各条记录,直到找到主键值为 6 的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。
File Tailer(8字节) 文件尾部
校验页是否完整。
前4个字节代表页的校验和。后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
总结
- 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 中。所以在一个页中根据主键查找记录是非常快的,分为两步: - 通过二分法确定该记录所在的槽。- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
- 每个数据页的 File Header 部分都有上一个和下一个页的编号,所以所有的数据页会组成一个 双链表。
- 为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时 对应的 LSN 值,如果首部和尾部的校验和和 LSN 值校验不成功的话,就说明同步过程出现了问题。
感谢您的阅读,如果您感觉本篇博客还不错,请帮忙留言+点赞+收藏呗。~~
版权归原作者 Hi-Sunshine 所有, 如有侵权,请联系我们删除。