前言
前文我们已经明白了行数据,如何在页存储中分布的行是怎么存储的。
正文
那么页结构又是怎么样的呢?
这里我们唯一知道的就是user records 就是实际存储的行记录内容。
这里让我觉得有意思的是infimum 和 suprenum,分别是最小记录和最大记录,这两个是用来做什么呢?
我估计是用来查询的。
还有一个就是free space,这个猜想应该是用来扩缩page direcotry和 user records。
终点,page directory 某些记录的相对位置,估计也是用来查询的。
在页的7个组成部分中,我们自己存储的记录会按照我们指定的 行格式 存储到 User Records 部分。但是在一开
始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就
是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部
被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新
的页了,这个过程的图示如下:
里面的数据大致是这样:
一个链式结构。
这里可以看到两条特殊的记录,infimum 和 supremum 这两条记录是天生就有的,还记得我们说占用26个字节嘛。
怎么来的呢?
比如最小字节:5个固定字节是记录信息,8个字节存储的是infimum。最大记录也是这个,所以加起来是26个字节。
然后我们来看一下查询操作,如果2被删除了呢? 那是怎么样的呢?
这条记录只是标记而已。
然后最大记录的n_owned 变成了4.
如果再把这条数据插入回去呢,那么是怎么样的呢?
又回来了。
当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成
一个垃圾链表,以备之后重用这部分存储空间。
然后还有一个值得注意的是,nextrecord指向的是中间位置,也就是记录头和列数据之间,这是为了方便读取两边的数据。
这也是为啥变长字段长度列表null值列表是逆着写的。
因为读取出来是byte[] 数组,那么读取的是只需要--,那么就是顺序的了。
还有一个值得注意的就是heap_no 就是编号。
接下来page directory,这个对于查询是非常重要的。
对于一条有序的链表而言呢,我们怎么去查询呢?
链表天生有一个限制,那就是得从一头到另外一头。
那么怎么能快一点呢? 那就是记录链表的一些位置,这些位置也是顺序的,也就是将这条链表切成一小段一小段。
-
将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
-
每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记
录,也就是该组内共有几条记录。 -
将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近 页 的尾部的地方,这个地方就是所
谓的 Page Directory ,也就是 页目录 (此时应该返回头看看页面各个部分的图)。页面目录中的这些地址
偏移量被称为 槽 (英文名: Slot ),所以这个页面目录就是由 槽 组成的。
也就是下面这样的。
是的,设计 InnoDB 的大叔们对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,
最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
之后每插入一条记录,都会从 页目录 中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对
应的记录的 n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一
个5条记录。这个过程会在 页目录 中新增一个 槽 来记录这个新增分组中最大的那条记录的偏移量。
分组大概是这样的:
这里是一个简单的前开后闭的分组方式。
我们有一些思维就是如何分组的问题,比如说一条线,那么这个时候肯定会有各个点,那么这些点中如何分组呢?
很多人会想,两点之间分组,那么问题来了,这个点怎么处理呢? 这个时候就出现了,到底前开后闭环,还是前闭后开呢?
只需要考虑这个问题行,那么无论是那边,都有一个点作为一个组的存在,这是正常的。
-
通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
-
通过记录的 next_record 属性遍历该槽所在的组中的各个记录。
这个时候槽里面是不需要去记录排序位置的,二分法上面而言就是找到槽2,然后找到第8点记录进行对比,
如果比第8条小,那么往前,找1,同理这样找到槽。如果比1对应的记录大,那么就是槽2。
总之就是这么找,肯定能找到滴。
然后找到槽之后就开始遍历了,比如上门是槽2,那么遍历位置就是从槽1开始。
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第
一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫 Page Header 的部分,它是
页 结构的第二部分,这个部分占用固定的 56 个字节,专门存储各种状态信息。
大概是下面这些:
这些是用来干什么的呢?
用来做一些参考的,后续中就知道这里面有啥用了。
我们不需要去关心,可能是一些算法优化之类的吧,我们好像不做数据库,不用关心。
page header 我们可以说是对这一页的一些统计。
那么对外部来说,其就是问题,那么文件意义上的页,有哪些参数呢?
其实page header 和 file header 分别对应是是数据库存储的页和对于文件存储的页进行区分的。
有这些,不用去记录:
然后还有一个File Trailer:
基本是用来检验的。
我们写入磁盘的时候呢,磁盘的页可能是4k,反正就是有各种情况,我们在写入16k的时候发生了物理异常。
那么这个时候我们怎么排查这个问题呢? 因为有file header 有个检验位,因为header 先写入,如果header 和 trailer的检验和是一样的,那么就是写入没有问题。
否则就是有问题,然后需要一些修复啥的。至于怎么修复的,那就可能找到一些日志啥的吧。
后4个字节代表页面被最后修改时对应的日志序列位置(LSN) 猜测,可能是找到修改这页的日志,然后从这个位置开始,然后开始回放啥的。
这是mysql的人的设计,我们暂时用不到。我们只需要知道大致原理就行了。
结
就是这么回事吧,下一节,看下mysql 是怎么把这些页整合起来的。