当前位置: 首页 > 科技观察

MySQL页面完全攻略——简单深度页面退出原理

时间:2023-03-12 07:47:07 科技观察

之前写过一些关于MySQL的InnoDB存储引擎的文章,里面多次提到了页面(Pages)的概念,但只是简单的提了一下.比如我之前在讲InnoDB的内存结构的时候提到过,但是当时的重点是内存架构,所以没有深入展开。发现pages需要被多次提及,所以就拿了一篇文章详细说说InnoDB中的pages。什么是页面?首先,我们要知道页(Pages)是InnoDB中数据管理的最小单位。BufferPool中存储的数据是一页一页的。再比如,当我们要查询的数据不在BufferPool中时,InnoDB会将记录所在的整个页面加载到BufferPool中;同理,将BufferPool中的脏页刷入磁盘时,也是以页为单位刷入磁盘。对BufferPool不了解的,或者有兴趣的可以去文章开头给出的链接熟悉一下页面概况。我们向MySQL中插入的数据,最终都会存储在页面中。在InnoDB的设计中,页面是通过双向链表连接起来的。页面中存储的逐行数据通过单向链表连接。上图中的UserRecords区域用来存放行数据。那InnoDB为什么要这样设计呢?假设我们没有页面的概念,那么我们在查询的时候,如何快速的查询到几千条数据的结果呢?众所周知,MySQL的性能是好的,如果没有分页,我们就只能逐项遍历数据了。页面如何实现快速查询?在当前页面中,可以通过连接UserRecords中每条记录的单链表来遍历。如果在当前页没有找到,可以通过下页指针快速访问。跳至下一页查询。Infimum和Supreme有人可能会说,你不是通过遍历UserRecords来解决问题的,你只是简单地将数据分组。如果我的数据根本不在当前页,那我还需要遍历上一页的所有数据吗?这太低效了。当然MySQL也考虑到了这个问题,所以其实在页面中还有一个区域叫做TheInfimumandSupremeRecords,分别代表了当前页面最大和最小的记录。有了InfimumRecord和SupremeRecord,现在查询不需要遍历某个页面的所有UserRecords,只需要将这两条记录与要查询的目标记录进行比较即可。比如我要查询的数据是id=101,显然不在当前页面。然后可以通过下一页指针跳转到下一页检索。使用PageDirectory,可能有人又要说了,你的UserRecords不都是单链表吗?即使我知道我要找的数据在当前页面,最坏的情况下,我还是要一条一条地遍历100条记录。我可以找到多少次我正在寻找的数据?这叫效率高吗?不得不说,这确实是个问题,但是MySQL已经考虑到了这个问题。是的,一个一个遍历确实效率很低。为了解决这个问题,MySQL在页面中又增加了一个区域PageDirectory。顾名思义,PageDirectory是一个有很多槽(Slots)的目录,每个槽都指向UserRecords中的一条记录。可以看到,每隔几条数据就会创建一个slot。事实上,我图中显示的数据是非常严格按照其设置的。在一个完整的页面中,每6条数据就会有一个Slot。不知道PageDirectory的设计是否让你想起另一种数据结构——跳表,不过这里只有一层索引抽象。当有新数据加入时,MySQL会创建相应的Slot。使用页面目录,可以对页面的数据执行粗略的二分查找。至于为什么粗,毕竟PageDirectory不是完整的数据,二分查找的结果只能是大概的位置。找到这个大概位置后,还需要回到UserRecords继续逐一遍历匹配。不过这个效率比我们刚开始讲的原始版本要高很多。页面的真实面貌如果我在文章开头就直接抛出页面的各种组件和概念,首先我自己是接受不了的,显得很生硬。其次,不熟悉网页的人应该无法理解网页为什么要这样设计。于是我按照查询一条数据的一套思路,把页面的大概样子呈现给大家。其实页面上还存储了很多其他的字段,还有其他的区域,但是这些都不会影响我们对页面的理解。因此,在对页面有了更清晰的认识之后,我们就可以看一下真实的页面是什么样子的。上图是页面的实际构成。除了我们之前提到的,还有一些之前没有讲到的东西,比如FileHeader、PageHeader、FreeSpace、FileTailer。让我们一一看看。FileHeader其实FileHeader上面已经讲过了,只是不叫这个名字而已。上面说的上一页指针和下一页指针其实都属于FileHeader,除此之外还有很多其他的数据。其实我比较抗拒列出一堆参数,告诉你大小是多少,有什么用。我们要详细了解页面,其实我们暂时只需要知道两个即可,分别是:FIL_PAGE_PREVFIL_PAGE_NEXT这两个变量就是上面说的上一页指针和下一页指针,都是指针。为了方便大家理解,其实就是页面在磁盘上的偏移量。PageHeader相对于FileHeader,PageHeader中的数据我们更为熟悉。我在这里画了一张图来详细列出内容。这里全罗列是因为理解这些参数的含义以及我们为什么要设置这些参数可以更好的帮助我们理解页面的原理和结构,看图说话就行了。这里也想吐槽一下,太多博客写的太死板了,比如参数PAGE_HEAP_TOP,这里的HEAP在很多博客里直接叫heap。这就好比给Init写个注释叫初始化,还是不写为妙。其实研究一下就会知道,这里的heap其实指的就是UserRecords。有两个参数可能有点混乱。分别是PAGE_N_HEAP和PAGE_N_RECS,分别是当前UserRecords中的记录条数。唯一的区别是PAGE_N_HEAP包含了标记为删除的记录,而PAGE_N_RECS是实际我们可以在上面查询到的所有数据。Infimum&SupremumRecords如前所述,Infimum&SupremumRecords会记录当前页面的最大和最小记录。事实上,它并不准确。更准确的描述是最小记录和最大记录之间的开区间。因为实际上InfimumRecords会小于当前页中的最小值,而SupremeRecords会大于当前页中的最大值。UserRecords用户记录可以说是我们平时接触最多的部分,毕竟我们的数据最终都在这里。页面初始化后,UserRecords中没有数据。随着系统的运行和数据的产生,UserRecords中的数据会不断膨胀,相应的FreeSpace会逐渐减少。关于UserRecords中的概念,我之前已经讲过了。这里我只说一个我认为很关键的点,那就是顺序。我们知道,在聚簇索引中,Key实际上会按照PrimaryKey的顺序排列。在用户记录中会相同吗?当我们向UserRecords中插入一条新数据时,我们是否也会将现有数据按照PrimaryKey的顺序重新排序?答案是否定的,因为它会降低MySQL的处理效率。UserRecords中的数据是通过单链表的指针来保证的,也就是说该行数据在磁盘上的实际表现是按照插入的顺序排队的,先有数据在前,数据在后在后面。只是UserRecords中的行数据之间的一个单链表按照PrimaryKey形成了一个顺序。用一张图来表示,大致如下:FreeSpace其实是变相在其他模块讨论的。最初,用户记录是完全空的。当有新数据进来的时候,就会来到FreeSpace申请空间。当FreeSpace为空时如果没有空间,说明需要申请一个新的页面,没有什么特别的。PageDirectory这与上面讨论的内容无关,所以我直接跳过了。FileTrailer主要用于防止在页面刷入磁盘的过程中由于极端意外(网络问题、火灾、自然灾害)导致数据不一致,即脏页。里面只有一个组件:FileTrailer到此为止,我想页面的所有内容都差不多涵盖了。了解页面底层原理,个人认为有助于我们更友好、更合理地使用MySQL。从而发挥它该发挥的极致性能。