InnoDB - 页结构
文章目录
- InnoDB - 页结构
- 1. InnoDB页简介
- 2. InnoDB页结构
- 2.1 User Records(数据)
- 2.2 Page Directory(页目录)
- 2.3 Page Header(页头部信息)
- 2.4 File Header(文件头)
- 2.5 File Trailer(文件尾)
- 3. 总结
在阅读本文章之前,你需要了解的前置知识:
- InnoDB - 行格式
1. InnoDB页简介
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正的对数据的处理过程是发生在内存中的。
即:将磁盘中的数据读取到内存中进行操作。
- 读请求:将磁盘中的内容读取到内存中。
- 写请求:将内存中的被修改后的数据写回磁盘中。
我们知道,读写磁盘的速度非常慢,与内存操作差了几个数量级,所以当我们想要从表中获取数据时,InnoDB会一条一条的把记录从磁盘中都出来吗?不是,InnoDB采取的方式是:将数据划分为若干页,以页为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为16k。
在这种情况下,MySQL一次最少会从磁盘中加载16k的数据到内存中,说明白点:即使你只select了一条数据,与该条数据同一页的所有数据都会被加载(从磁盘加载到内存)。
现在我们就可以梳理一下MySQL的InnoDB引擎中页的概念:InnoDB管理存储空间的基本单位、用于磁盘与内存交互的最小单位。
2. InnoDB页结构
16k的页被分为好多个部分,并不是16k全部存储数据的。以下为页的基本结构。
共有7个部分,他们的大概功能如表格所示:
你可以先看表格了解一下它们都是干啥的,下面会详细的描述它们的作用。
名称 | 中文名 | 作用 |
---|---|---|
File Header | 文件头部 | 页的一些通用信息 |
Page Header | 页面头部 | 数据页专用的一些信息 |
Infimum + Supermum | 最小记录和最大记录 | 两个虚拟的行记录 |
User Records | 用户记录 | 实际存储的行数据内容,初始为空,随着记录的增加,从Free Space中挪用空间 |
Free Space | 空闲空间 | 页中未使用的空间,为User Records提供空间 |
Page Directory | 页面目录 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 校验页是否完整 |
我们接下来并不打算按照页中各个部分的出现顺序来依次介绍它们,因为各个部分中会出现很多大家目前不理解的概念.
2.1 User Records(数据)
在页的7个组成部分中,我们自己的数据(student、user)会存储在User Records中。但是在一开始生成页的时候不会为User Redords分配空间,每当我们插入一条数据,都会从Free Space分出部分空间到User Redords来存储这条数据,直到Free Space被User Records全部替代,说明这个页用光了,就会创建新的页。
在上一篇InnoDB - 行格式中,我们介绍了InnoDB行格式,说到了每一行其实有6个隐藏字段,其中有一个隐藏字段叫做:记录头信息。
记录头信息中又有很多字段,在这里会涉及到的有5个:(此图省略了其他隐藏字段)
delete_mask:该记录是否被删除。
min_rec_mask:是否为B+树的每层非叶子节点中的最小记录。
n_owned:有几个记录属于这个记录。每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录。
录,也就是该组内共有几条记录。
heap_no:在页中的下标(第几条数据)。
next_record:表示下一条记录的相对位置。
我们想这张表中插入3条数据:
insert into users values(1, 'aaaa'),
(2, 'bbbb'),
(3, 'cccc');
于是这几条数据就如下图所示连接:
由于还没有接触到,所以有几个字段在图中是不重要的,这里只需要注意两个字段:heap_no、next_record。
很明显,next_record是指向下一条记录的指针。
heap_no是此行数据在页中的下表,但是我们知道下标一般是从0或者1开始的,哪有从2开始的呢?怎么不见 heap_no 值为 0 和 1 的记录呢?
其实MySQL的设计者在实现这部分的时候耍了花样:页结构中有这样一个字段:Infimum、supermum。它们的中文名分别是最小记录和最大记录。
现在你应该猜到序号0和1都归谁了吧?就是最小记录和最大记录。
那么刚才那张图完善一下就是:
如图所示,MySQL隐藏的最小记录指向我们添加的主键最小的记录,形成一个链表,最后我们添加的主键最大的记录指向MySQL自带的最大记录。
即:infimum -> User Records -> supermum。
从中删除一条数据,整个链表的结构就会发生变化,假如从中删除id为1的数据,
不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。
当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。
即:未删数据和删除数据都会组成链表。
你会不会觉得next_record很奇怪?它竟然指向隐藏字段和真实数据之间的部分,而不是指向隐藏字段开头部分
因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。
我们知道,隐藏字段中信息的记录都是逆序的(参考InnoDB - 行格式),它从右向左读就可以读取到正序的信息了!
2.2 Page Directory(页目录)
刚才说了,数据之间会形成链表,方便查找,但是链表很明显效率很慢。MySQL的索引使用的是B+树,这里我们讲的是数据页,不是索引页,所以没有用到B+树,但是MySQL的设计者还是采用了一定的方式加快数据的查询。
也就是查目录的方式。
打一个比较形象的比喻:
进了大学,班长记不住也不想记住所有同学的名称,于是他将全班同学按照学号排序、分组,每个组大概4-8人,组里学号最大的那个人当组长,班长建立一个组长群,将组长拉进去,有啥事就直接通知组长而不会通知组员,想要找某个人的时候就直接在组长群里说:xxx在哪一个组?组长叫他做一下青年大学习~
MySQL采用了以下方式:
- 将所有未删除的数据划分为不同的组
- 每个组的最后一条记录的头信息中的 n_owned 属性表示该组内有多少条记录
- 将每个组的最后一条记录的地址偏移量单独提取出来按照顺序存储到 Page Directory 中(也就是页目录),我们将这些地址偏移量称为 槽。
这里不要被搞糊涂了:一个页中有很多条数据,这些数据会按照链表的格式存储,我们将这些数据分为很多组,每个组的最后一条记录
的地址抽取出来记录在 Page Directory 中。
这里的整个班就是一个页,各个组就是MySQL中的组,组长就是每个组的最后一条数据,组长群就是 Page Directory。进入组长群后的组长被称为“槽”。
注意:
- 槽0中的数字为91,代表最小记录的地址偏移量为91字节。
- 槽1中的数字为122,代表最大记录的地址偏移量为122字节。
- 槽0指向最小记录,代表最小记录单独一组,也就只有最小记录这一条数据,所以它的n_owned值为1。
- 槽1指向最大记录,代表最大记录以及之前的数据为一组,所以它的n_owned为4
那么为什么最小记录单独一组呢?因为规定,MySQL对于每个分组中的记录个数是有规定的:最小记录所在的分组只能由1条记录,最大记录所在的分组可以有18条记录,剩下的其他分组可以有48条记录。
我们看一下数据多的时候:
如图所示,当有15条数据时,总记录数为17,被分为5个组,共有5个槽位,槽位对应的数据条数分别为:1、4、4、4、3。
但是!插入的时候是有讲究的!
- 初始情况下是没有用户数据的,只有两个组:最小记录组与最大记录组。
- 之后没插入一条记录,都会从页目录(槽位)中找到主键值比本记录大,并且差值最小的槽位,然后把该槽位指向的记录的 n_owned 值加一,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 当组中的记录数等于8个时,再次插入会将本组分为两个组,加入分为4组和5组,第四组有4条记录,第五组有5条记录。因为拆分了,所以页目录中会增加一个槽位。
弄清了分组的情况,现在我们可以来查找了,假如想要查找到 id=8 的数据。从图中可以看到它在第三组。
因为各个槽指向的记录的主键都是从小到大排列的,所以我们可以使用二分法。
此时共有5组,编号为 0、1、2、3、4 ,记 low = 0,high = 4
- (low + high)/ 2 = 2,第二个槽位对应的记录的id为9,太大了,high改为2
- (low + high)/ 2 = 1,第一个槽位对应的记录的id为5,太小了,low改为1
- 因为 high - low = 1,可以说明目标数据就在 第二组 中,我们只需要获取low槽位对应的数据(第一组的最后一个数据),向下找一个数据就到达第二组的最小数据,此时high指向的槽位的记录数是第二组的最大记录,获得了最大与最小,id=8的数据还不是唾手可得?
所以在一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到改槽中主键值最小的那条记录。
- 通过记录的 next_record 属性遍历该槽,找到目标数据。
2.3 Page Header(页头部信息)
数据页头部信息,行数据字段记录本行的基本信息,那么页数据也会有专门的字段记录本页的信息,而Page Header就是用于记录一个数据页的状态信息,比如本页存储了多少个槽、多少个数据等等…
具体有什么字段,来看表格:(由于字段太多了,这是精简过的字段)
名称 | 作用 |
---|---|
Page_N_Dir_Slots | 该页的页目录中的槽的数量 |
Page_N_Heap | 本页中的记录的数量(删除&未删除) |
Page_Level | 当前页在B+树所处的层级 |
Page_Index_id | 本页属于哪个索引 |
2.4 File Header(文件头)
File Header是每种页都有的属性,其中重要的字段如:本页编号、上一页编号、下一页编号、校验和。
上一个与下一个页的编号:可以将所有数据页组成一个双链表,遍历就很方便了。
File Header中有一个很重要的字段:校验和。
什么是校验和?
对于一个很长很长的字符串来说,我们会使用某种算法来计算一个比较短的字符串来代替它,比如我们通常用文件的md5编码来初步比较文件是否相等。
我们想比较两个很长的字符串的时候,可以先比较他们的校验和,如果连校验和都不一样,那这两个字符串就肯定不一样。这样就省去了一个字符一个字符比较消耗的时间。
同时,File Header的校验和还会和File Trailer的校验和配合工作,下面会介绍。
2.5 File Trailer(文件尾)
File Trailer跟File Header一样,有一个校验和,那么这个校验和到底是什么作用呢?
我们知道InnoDB存储引擎会将数据从磁盘中读到内存中,以页为单位。如果数据在内存中修改了,那么MySQL势必要把他们再写回磁盘中,写回的过程中万一出现了什么错怎么办?这不是尴尬了吗?为了检验一个页是否完整(也就是同步的时候是否出现同步了一半的情况),MySQL就在页的头部和尾部都加了校验和这个东西。
每当一个页在内存中修改了,在同步之前就要把它的校验和算出来。File Header在页的最前面,所以校验和会被优先同步,如果完全写完,那么尾部的校验和也会写进去,这没问题。万一中断了,那么File Header的校验和是已经修改过的页,File Trailer的校验和是原先的页,二者不相同,说明同步过程中出现bug。
(其实还有一个字段,现在先不管)
3. 总结
-
InnoDB为了不同的目的而设计了不同的页类型,我们把存放数据的页称为数据页。
-
数据页可以被分为7个部分:
- File Header:表示页的一些通用信息
- Page Header:表示数据页专有的信息
- Infimum + Supermum:两个虚拟的伪记录,分别表示页中的最小记录和最大记录
- User Records:存放我们插入的数据
- Free Space:页中可以被我们使用的数据
- Page Directory:页中的某些记录的相对位置,其中存放很多槽位
- File Trailer:页尾,与File Header一起提供校验和来检查同步状态
-
每个数据页的File Header都有上一个与下一个页的编号,所以所有的数据页会组成一个双链表
-
User Records中的每一条记录有next_record,它将所有记录串联为一个链表(已删除的和未删除的,共两个链表)
-
为保证内存到磁盘的同步的完整性,在数据页的首部和数据页的尾部都会存储页中数据的校验和