Innodb资料页结构-Part2(页面目录、页面表头、档案表头、档案结尾)

资列页(16kb大小)的结构有7个部分

1.使用者纪录(user records)
2.空闲空间(free space)
3.页面中最小与最大的纪录(infimum+supermum)
4.页面目录(page directory)
5.页面表头(page header)
6.档案表头(file header)
7.档案结尾(file trailer)

前文提到前三个的部分,今天会当作大家已经熟悉前面的内容,继续说明剩下的部分,在过程中如有不清楚的地方就再回去复习吧。

页面目录

根据之前的学习我们已经知道资料页的结构是一个按照主键大小排序的单向链结串列。
此时我们想要查询资料的话,最直观且最差的做法就是一笔一笔依序查询,当资料页有n笔我们新增的纪录,那就查询n次吧...

大家应该都认同这不是一个好的解法,我们来学习Innodb的工程师是怎麽设计的
设计的思维就像是我们在看一本书时,最前面会有各章节的页码,透过其我们可以知道要查询的内容从那一页开始找比较快。
Innodb的工程师就是这样针对所有纪录去分组(就像书的各章节)并定义各组的槽(就像章节的页码)。

具体的作法细节如下:

  1. 将所有纪录分组(包含infimum+supermum纪录,但不包含删除的纪录)。
  2. 将每组最後一笔纪录(也是主键值最大的那笔)的位址偏移量(该纪录的真实纪录与第0个位元组之间的距离)提取出来存在接近页尾部的地方。这些位址偏移量也称为槽,每个槽占2个位元组,页目录就是由这些槽组成。

分组有一个规定,infimum纪录所在的组只有它自己一笔纪录,supermum纪录所在的组包含它自己只能有1到8笔纪录,其余的组别则为4到8笔纪录。
这边进一步补充说明前面第一点分组的细节

  1. 页初始的时候只有infimum跟supermum纪录,分属两个组,页目录也只有两个槽,分别代表infimum纪录跟
    supermum纪录在页面中的位址偏移量。
  2. 想像一下分组是由上而下(主键值由小而大)依序紧紧相连的情况。新增一笔纪录的时候,会去找寻符合(值在界限内)的槽插入。

举一个例子来说明:
现有16笔纪录

mysql> insert into ryan_demo2_table(c1,c2,c3) values (1,100,'a'),(2,200,'b'),(3,300,'c'),(4,400,'d'),(5,500,'e'),(6,600,'f'),(7,700,'g'),(8,800,'h'),(9,900,'i'),(10,1000,'j'),(11,1100,'k'),(12,1200,'l'),(13,1300,'m'),(14,1400,'n'),(15,1500,'o'),(16,1600,'p');
Query OK, 16 rows affected (0.01 sec)
Records: 16  Duplicates: 0  Warnings: 0

mysql> select * from ryan_demo2_table;
+------+------+------+
| c1   | c2   | c3   |
+------+------+------+
|    1 |  100 | a    |
|    2 |  200 | b    |
|    3 |  300 | c    |
|    4 |  400 | d    |
|    5 |  500 | e    |
|    6 |  600 | f    |
|    7 |  700 | g    |
|    8 |  800 | h    |
|    9 |  900 | i    |
|   10 | 1000 | j    |
|   11 | 1100 | k    |
|   12 | 1200 | l    |
|   13 | 1300 | m    |
|   14 | 1400 | n    |
|   15 | 1500 | o    |
|   16 | 1600 | p    |
+------+------+------+
16 rows in set (0.01 sec)

其实在资料页里面有18笔纪录(包含自动建立的infimum跟supermum纪录)
而这里的分组会是(以c1栏位主键值来看)
infimum纪录自己1组(槽0 对应的主键值为infimum)
1、2、3、4四笔1组(槽1 对应的主键值为4)
5、6、7、8四笔1组(槽2 对应的主键值为8)
9、10、11、12四笔1组(槽3 对应的主键值为12)
13、14、15、16四笔纪录跟supermum纪录1组(槽4 对应的主键值为supermum)
总共5组

在这样的情境下,我们想搜寻一笔主键值为6的纪录,搜寻的过程会是以下这样的。
1.一开始low为0,high为4,计算中间槽的位置(0+4)/2=2,查看槽2对应的主键值为8,因爲6小於8,所以high变为2(界限缩小),而low不变一样为0
2.重新计算中间槽的位置(0+2)/2=1,查看槽1对应的主键值为4,因为6大於4,所以low为1(界限缩小),而high不变一样为2
3.因为high-low为1,表示要搜寻的纪录就在槽2的组别里面,这时只要找到槽2主键值最小的那一笔纪录,沿着单向链结串列历遍槽2的所有纪录即可找到我们要的纪录

本来要比对16次的纪录变成只要历遍5,6就找到6了,就算不幸为此组最後一笔,1组纪录笔数最多为8笔,因此最多也就8次,大大的提升了搜寻的速度。

页面表头

Innodb为了想得到资料页的纪录状态定义了页面标头,方便知道情况。
这边总共有14个状态(占用固定56位元组)但我不会全部列出来,我觉得现在看也看不懂的就不赘述了,等未来有遇到再来补充大家也比较清楚。我这边就特别提几个应该要知道且实用的状态。

  • PAGE_N_DIR_SLOTS 页目录中的槽数量(占用2个位元组)
  • PAGE_HEAP_TOP 还未使用的空间最小位址,也就是説从该位址之後就是free space(占用2个位元组)
  • PAGE_GARBAGE 已删除纪录占用的位元组数(占用2个位元组)
  • PAGE_LAST_INSERT 最後插入纪录的位置(占用2个位元组)
  • PAGE_DIRECTION 表示最後一笔纪录插入的方向。加入新纪录的主键值比上一笔的大就是右边,反之为左边(占用2个位元组)
  • PAGE_N_DIRECTION 一个方向连续插入的纪录数量(占用2个位元组)
  • PAGE_N_RECS 该页中使用者纪录的数量(不包含infimum跟supermum纪录及被删除的纪录,占用2个位元组)

档案表头

前面的页面表头是纪录资料页的各种状态,而档案表头则不局限於资料页,包含了其他各种类型页面的纪录状态。这里一样只说明几个比较重要的状态。

  • FIL_PAGE_SPACE_OR_CHKSUM 校正码(什麽是校正码?当有一个很长的位元组串,我们会透过演算法计算出较短的值来代表,这个短值就是校正码。好处是当要比对两个很长的位元组串前,先比较校正码就可以大幅降低时间)
  • FIL_PAGE_OFFSET 独立页号(像我们的身份证号一样)
  • FIL_PAGE_TYPE 页的类型。之前有提过除了资料页外还有很多其他的页类型。
  • FIL_PAGE_PREV和FIL_PAGE_NEXT 有时候资料量非常大无法一次性的分配很多页,资料可能储存在不连续的页中,因此有了这两个属性把不连续的页串连起来,形成一个双向链结串列。要注意的是并不是所有类型的页都有这两个属性唷,但资料页有。

档案结尾

我们知道Innodb会先把资料从磁碟读取到记忆体去更新(因为磁碟速度太慢),更新完毕後在刷新回磁碟。
但如果在记忆体更新後,发生意外(断电等),导致没有刷新回磁碟,两边资料不一致的bug就尴尬了。
所以Innodb在每个页的尾部增加了档案结尾的部分。占8位元组包含两个部分(校正码4位元组及最後被修改时对应的LSN的後4位元组[LSN是新名词之後会再说明,先大概知道有这东西就好])。
透过比对档案表头与档案结尾的校正码及LSN验证来确保资料成功刷新。

今天的内容较多,但这些都是基础且很重要的东西,大家要好好熟悉,因为这对接下来要说明的内容都是必要的前提知识。在一起好好加油吧~耶~吃苍蝇去!


<<:  Android学习笔记02

>>:  Day 07-Terraform 写起来不够 DRY 的问题,这解 Terragrunt 你试试看

[Day24] 运用 VS Code 组合键加快编辑速度 - 文字编辑篇

VS Code组合键真的非常多,才发现每天抽空的时间没办法练习完,重新编辑分成『介面操作』和『文字编...

OpenStack Neutron 介绍 3

本系列文章同步发布於笔者网站 上一篇介绍了 Neutron 的网路的概念,接下来将会接续介绍 Neu...

前言与接下来的学习历程

前言 一开始是为了一些蠢事而做的,至於是什麽蠢事,这边就暂且不提,後来自己看见Meet6机器人的强大...

[Day 18] 阿嬷都看得懂的 CodePen 怎麽用

阿嬷都看得懂的 CodePen 怎麽用 VSCode 已经很棒了,不过最近比较流行的是云端协作。以目...

Day 14:怎麽在 Angular 使用 Bootstrap?

由於在未来的专案有机会使用到 Bootstrap,所以就藉这个机会来介绍一下如何在 Angular ...