Mysql存储引擎---InnoDB数据页结构

一、数据页结构

数据页代表的这块 16KB 大小的存储空间可以被划分为多个部分,不同部分有不同的功能,各个部分如图所示:
在这里插入图片描述
一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。
在这里插入图片描述

记录的存储

在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。如下图所示:
在这里插入图片描述

记录头信息

名称 大小(bit) 描述
预留位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 表示下一条记录的相对位置

例如:创建一个表

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

INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'),(4, 400, 'dddd');

在这里插入图片描述
在这里插入图片描述

各条记录在 User Records 中存储的时候并没有空隙,这里只是为了观看方便才把每条记录单独画在一行中。

  • delete_mask
    这个属性标记着当前记录是否被删除,占用1个二进制位,值为 0 的时候代表记录并没有被删除,为 1 的时候代表记录被删除掉了。

这些被删除的记录之所以不会立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的 可重用空间 ,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。

  • heap_no
    这个属性表示当前记录在本页中的位置
  1. 我们插入的4条记录在本页中的位置分别是: 2 、 3 、 4 、 5
  2. InnoDB自动给每个页里边儿加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录(0、1)。这两个伪记录一个代表最小记录,一个代表最大记录。
  3. 对于一条完整的记录来说,比较记录的大小就是比较主键的大小。比方说我们插入的4行记录的主键值分别是: 1 、 2 、 3 、 4 ,这也就意味着这4条记录的大小从小到大依次递增。
  4. 伪记录或者虚拟记录由5字节大小的 记录头信息 和8字节大小的一个固定的部分组成的。在这里插入图片描述
    由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的 User Records 部分,他们被单独放在一个称为 Infimum + Supremum 的部分
  • record_type
    这个属性表示当前记录的类型,一共有4种类型的记录, 0 表示普通记录, 1 表示B+树非叶节点记录, 2 表示最小记录, 3 表示最大记录。

我们自己插入的记录就是普通记录,它们的record_type 值都是 0 ,而最小记录和最大记录的 record_type 值分别为 2 和 3

  • next_record
    它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。

下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(最大记录)。
如图:我们的记录按照主键从小到大的顺序形成了一个单链表在这里插入图片描述
不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。如:删掉第2条记录后的示意图:
在这里插入图片描述
主键值为 2 的记录被我们删掉了,但是存储空间却没有回收,如果我们再次把这条记录插入到表中, InnoDB 并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。(当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。)

问:next_record这个指针问什么要指向记录头信息和真实数据之间的位置呢?为什么不干脆指向整条记录的开头位置,也就是记录的额外信息开头的位置呢?
答:因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。而且变长字段长度列表、NULL值列表中的信息都是逆序存放的,这样可以使记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓存的命中率。

二、页结构详解

1. 页目录(Page Directory)

当我们查询一条记录时:

SELECT * FROM page_demo WHERE c1 = 3;

最笨的办法:从 Infimum 记录(最小记录)开始,沿着链表一直往后找。因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了。但是如果一个页中存储了非常多的记录,这么查找对性能来说还是有损耗的。

我们平常想从一本书中查找某个内容的时候,一般会先看目录,找到需要查找的内容对应的书的页码,然后到对应的页码查看内容。 InnoDB为我们的记录也制作了一个类似的目录。

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

例如:page_demo表中正常的记录共有6条, InnoDB把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录。

最小和最大记录的头信息中的 n_owned 属性:

  • 最小记录的 n_owned 值为 1 ,这就代表着以最小记录结尾的这个分组中只有 1 条记录,也就是最小记录本身。
  • 最大记录的 n_owned 值为 5 ,这就代表着以最大记录结尾的这个分组中只有 5 条记录,包括最大记录本身还有我们自己插入的 4 条记录。
    在这里插入图片描述
    用箭头指向代替地址偏移量,单纯从逻辑上表示记录和页目录的关系:
    在这里插入图片描述

规定:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间

  • 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
  • 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的 n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
  • 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。

在一个数据页中查找指定主键值的记录的过程

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

2. 页面头部(Page Header)

Page Header 它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息
在这里插入图片描述

  • PAGE_DIRECTION
    假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION 。
  • PAGE_N_DIRECTION
    假设连续几次插入新记录的方向都是一致的, InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION 这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。

3. 文件头部(File Header)

Page Header是专门针对数据页记录的各种状态信息。

File Header 针对各种类型的页都通用,也就是说不同类型的页都会以 File Header 作为第一个组成部分,它描述了一些针对各种页都通用的一些信息。
在这里插入图片描述

  • FIL_PAGE_SPACE_OR_CHKSUM
    代表当前页面的校验和(checksum)。

校验和就是对于一个很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。

  • FIL_PAGE_OFFSET
    每一个页都有一个单独的页号,InnoDB通过页号来可以唯一定位一个页。
  • FIL_PAGE_TYPE
    这个代表当前页的类型。

InnoDB为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的数据页,其实还有很多别的类型的页。(数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页)

  • FIL_PAGE_PREV 和 FIL_PAGE_NEXT
    InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大, InnoDB可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。

并不是所有类型的页都有上一个和下一个页的属性,不过数据页 (FIL_PAGE_INDEX)是有这两个属性的,所以所有的数据页其实是一个双链表。
在这里插入图片描述

4. 文件结尾信息(File Trailer)

InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。

若在同步数据的时候因为某些因素同步被终端了(如停电)怎么办?所以为了检测一个页是否完整,InnoDB在每个页的尾部都加了一个File Trailer 部分,该部分由8个字节组成。

  • 前4个字节代表页的校验和
    这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
  • 后4个字节代表页面最后修改时对应的日志序列位置(LSN)
    这个部分也是为了校验页的完整性的。

为保证从内存中同步到磁盘的页的完整性,在页的首部和尾部都会存储页中数据的校验和和页面最后修改时对应的LSN值,如果首部和尾部的校验和与 LSN 值校验不成功的话,就说明同步过程出现了问题。

File Trailer与File Header类似,都是所有类型的页通用的。

三、总结

各个数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个 单向链表 ,每个数据页都会为存储在它里边儿的记录生成一个页目录 ,在通过主键查找某条记录的时候可以在 页目录 中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
在这里插入图片描述
其中页a、页b、页c … 页n 这些页可以不在物理结构上相连,只要通过双向链表相关联即可。

猜你喜欢

转载自blog.csdn.net/myjess/article/details/115529704