likes
comments
collection
share

揭秘MySQL数据页构造:探秘页中数据的奥秘

作者站长头像
站长
· 阅读数 16

随便聊聊

记录在页中是如何存储的

页是什么?

简单点来说,页实际上就是若干条记录所组成的一个更大的集合。使用这个大集合来操作数据有什么好处呢?这就要说道计算机中两个非常重要的假设在:

  1. 空间局部性:一旦某个存储单元被访问,其附近的存储单元也很有可能将被访问。这是因为数据通常是以数组、表为结构进行存储,而我们对数据的操作通常是连贯的,程序操作数据的方式通常也是通过循环来访问连续的数据。
  2. 时间局部性:如果某数据被访问过,不久以后该数据可能再次被访问。

根据这两条假设,如果某一条记录被访问到,那么我们就可以将这条记录左右的若干条记录一次性加载到内存里面,反正这些数据有非常大的可能会被访问到,不如直接梭哈,赢了单车换摩托,输了还是单车,稳赚不赔。

可能有的同学会不理解,如果同样读取10条记录,采用页来读取记录和直接读取记录,好像最终都是要访问相同大小的磁盘存储单元,甚至于页中还需要保存额外的存储信息,读取页所需要访问的存储单元数量应该大于直接访问对应的记录,为什么访问页能够提高 io 性能? 这是由于同一页的数据在磁盘上是连续存储的,访问页进行的是顺序 io,而10条记录在磁盘上就不一定连续了,进行的是随机 io。对于磁盘来说,顺序 io 的速度比随机 io 更快。

在 MySQL 中,默认情况下每一个数据页的大小将被设置为 16KB

掀开数据页的盖头

上面说了那么多页的好处,接下来我们就来快速猫一眼数据页的组成:

揭秘MySQL数据页构造:探秘页中数据的奥秘 数据页被划分成七个区域,分工合作,各司其职。下面这个表格简单的描述了一下不同区域的功能。

名称描述
File Header页的一些通用信息
Page Header数据页的专有信息
Infimum + Supermum最大记录和最小记录
User Records实际存储的行记录
Free Space页面中尚未使用的空间
Page Director页目录
File Trailer文件尾部

接下来我们就来具体介绍一下这些结构以及它们的作用。

记录才是根本目的

揭秘MySQL数据页构造:探秘页中数据的奥秘

在额外信息部分中,有一个字段叫做记录头信息,这一个字段中就包含着一条记录中所有的额外结构信息,所以记录的链表指针自然也被包含在这部分中。当然,记录头中还包括其他很多信息,不过现在暂时可以不用关心。那么在数据页中,我们的记录的存储方式就可以用一张图片简单的表示出来:

揭秘MySQL数据页构造:探秘页中数据的奥秘

接下来,对于记录的操作就等同于对链表进行操作,不过需要注意的是,在进行删除操作的时候,MySQL 进行了一些特别的优化。当删除某一条记录的时候, MySQL 并不会立刻回收这条记录所占用的空间,而是会在记录头信息中标记一下当前记录已经被删除,并且将这条记录加入到一个被称为垃圾链表的链表中。之后如果有新的记录插入到这个表中,这些记录所占用的空间就有可能被重新利用。

为什么不立刻将空间进行回收呢?如果删除的记录正好在链表的尾部,那么将空间回收到 Free Space 中确实很简单,但是如果删除的记录处于链表中间某个位置,那么回收这部分空间就需要重新排列删除节点之后的所有链表节点。这么做是在是不太值当。因此不如使用一个链表把他们记录下来,等待再次利用。

同时,为了方便对页中的记录进行管理,MySQL 在每一个页中设置了两条特殊的记录,InfimumSupermum,这两条记录是一个人为设置的下限和上限,用户插入的所有记录都大于Infimum 并且小于 Supermum,这两条记录不仅可以当做当前页面中所有记录的头结点和尾结点,同时在 MySQL 的事务中也将会发挥作用,这个我们后面在慢慢聊。为了方便管理,InfimumSupermum 将存放在一个专门为它们开辟的空间中。 那么,最后在页中记录的存储结构就如下面这张图。

揭秘MySQL数据页构造:探秘页中数据的奥秘

加速!加速!加速!

前面说到了记录在页中是采用链表进行存储的。链表虽然能够为记录的删除和插入中可以带来一定的方便,但是别忘了,在数据库的使用中,查询是至关重要的功能。而这正是链表的劣势,如果我们想要根据主键的值查询某一条记录,那么就只能沿着链表的一个节点一个节点的查找。如果一个页内存储了很多的记录,采用这种方法就太呆了。如何才能在既保留链表优点的同时还能提高查询的性能呢?MySQL 想出了一个办法,为同一页中的所有记录创建一个目录。 MySQL 首先将同一页中的记录分为若干个组,接下来开辟一个数组,数组中的每个元素将会维护两个数据,第一个数据是一个指向每一组第一条记录的指针,第二个数据是每组第一条记录的主键值。如下图所示:

揭秘MySQL数据页构造:探秘页中数据的奥秘

有了这个数组,搜索记录的时候,就可以通过先查找数组中的元素来确定记录所在的大致范围,这就像阅读一本书的时候,可以先通过书前面的目录来大概确认一下想要阅读章节的位置一样。接下来当我们想要查询页中的某一条记录,首先应该在这个数组中找到第一个大于欲查找记录主键值数组元素的前一个元素,因为我们想要查找的记录肯定被存放在这个元素所指向的组中。接下来只需要遍历这个组中的所有元素就可以找到我们想要的元素。这样子,原来需要遍历整个链表的记录才能够查询到的值现在最多只需要遍历一个组的记录就能找到,大大的增加了查询的性能。并且当我们检索目录的时候,由于采用的是数组的结构,还可以进一步通过二分查找算法提升查找对应组的速度。 MySQL 将这一个用于索引的数组统一存放在页的 Page Directory 中,将数组中的每一个元素称为槽。

页面头部、文件头部和文件尾部

写到这里,其实这篇文章想要说明的主要问题已经解决了,接下来简单说明一下数据页中剩余的三个部分:

Page Header

File Header 和 File Trailer

与 Page Header 的功能相同,File Header 同样负责存储管理信息。他们的不同点在于,Page Header 是数据页专属的,存放的是专属于数据页的管理信息。File Header 则是所有类型页的通用管理信息。在 File Header 的信息里,有两个功能比较重要。在 MySQL 中,记录是以页为单位进行存储的,一个表可能由若干个页组成,因此,为了保存页与页之间的关系,页与页之间同样采用链表的方式进行连接。

揭秘MySQL数据页构造:探秘页中数据的奥秘

同时,MySQL 中的数据最终将会被保存到磁盘等介质中进行持久化,但是保不齐在页刚写入一半的时候就发生断电、程序崩溃等无法预料的问题。这样,在进行读取的时候,这一页的数据实际上已近发生了污染。这样的数据实际上不应该被读取。为了应对这种突发情况,MySQL 在 File Header 和 File Trailer 中都设置了一个校验和字段。当读取页面数据时,首先分别从 File Header 和 File Trailer 中读取出校验和,比较两个校验和是否相同。如果 File Header 和 File Trailer 中的校验和相同,说明当前页面已经被完整写入到页面中,如果不相同,就说明当前页面存在不一致问题。

小结一下:对于MySQL中的每一条记录有以下几个知识点:

  1. 在 MySQL 中,使用页作为和磁盘交互的基本单位
  2. 记录在页中,使用链表的形式进行存储。
  3. 为了提升性能,MySQL 将同一页中的记录划分为若干组,并且为每个组建立索引来提升搜索效率。
  4. 为了避免页面未完全同步的问题,分别在 File Header 和 File Trailer 设置一个校验和字段,用于确认页是否同步。