前言
这一节我们来介绍缓冲池的内部结构。如果不清楚缓冲池是什么东西可以查看之前系列的第一篇文章。缓冲池最简单的理解为数据库磁盘文件在内存对应的映射,是一个十分重要的核心组件,缓冲池的内容和细节还是挺多的,这部分内容个人会限制篇幅让读者更好的消化。
概述
- Buffer pool的内部结构
- 数据页和缓存页的关系
- 数据页的描述信息是什么?
- Mysql是如何知道哪些数据页被加载了
- 脏页
- 脏页的伪代码实现以及脏页的介绍
- 重点
- 分清楚free list和flush list
熟悉整个buffer pool的结构图。
Buffer pool 结构
关于buffer pool的完整结构图如下,本文将会一一分解来讲述各个块的内容:
Buffer pool在mysql中地位
Buffer pool可以看作是一个内存结构的组件,可以理解为一大片的内存区域,在默认的情况下它是128m的空间大小。需要注意的是默认值比较小,通常情况下这个值是远远不够的。
从结构图来看缓冲池是非常核心的一个组件,因为mysql数据的操作不可能放到磁盘完成的,哪怕是固态硬盘也是不可能快过内存,缓冲池可以看作是数据操作的时候磁盘文件的数据的一对一映射,但是如果我们操作内存又会出现另一个问题,内存的操作是十分快的,但是硬盘的刷新速度更不上内存,所以就会出现内存和硬盘上的数据不一致的问题,种由于某些操作更新之后的内容更新过的数据页在mysql当中统称为脏页。
所以redo log、undo log、bin log这几个日志文件某种程度上可以认为是为了确保数据正确同步的策略而出现的。
数据页和缓存页
既然缓冲池是一块内存空间,那么数据是否在buffer pool中呢?我们的数据是如何放在buffer pool中的?
这里我们我们来回顾下数据库的逻辑结构,数据库分为表+字段+行的模式,一个表有很多行数据,那么数据页的内容就是多行?其实数据库抽取了一个叫做数据页的概念,多行数据会放到一个数据页内部,磁盘中有多个页,每一个页都有很多行数据合并到一起,最终我们更新数据就是找到某一个页的某一行。
数据页的默认大小:16kb,缓存页的大小也是16kb。
总结:buffer pool存放的是一个个数据页,也叫做缓存页,由于buffer pool是一个近似内存的缓冲池,所以硬盘的数据会转变为一个个缓存页缓存到这块“内存”当中。Buffer pool中一个缓存页和磁盘的一个数据页大小是对应的都是16kb。
缓存页的描述信息
虽然我们知道了缓存页的大小,缓存页里面存放的也是一行行的数据,但是缓存页自己是不知道这些信息的,这时候mysql引入了另一个数据块叫做缓存页的描述信息,在缓存页描述信息包含了下面的内容:
- 所属表空间
- 数据页编号
- 缓存页在缓冲池里面的所属地址
描述信息有多大呢?一个描述信息大概是缓存页的5%左右的大小,可能是800个字节左右的大小,默认128m的缓冲池。需要注意的是,为了防止数据页撑满缓冲池导致描述信息无法存放,mysql会给描述信息一些额外的内存空间保证描述信息可以记录所有的数据页(缓存页)。所以这里面128M不是完全固定的,会额外多个几M的缓存页描述信息。
描述信息如何存放?
描述信息和缓存页按照类似“对称”的结构进行存储,描述信息放在缓存页的最前面,缓存页则放在缓冲池的最后面,至于这样设计的原因一方面是尽可能让描述信息不干扰数据页的分配,另一方面是为了让缓冲池有“额外”并且足够的空间来存放描述信息。
如何尽量减少buffer pool的内存碎片?
当缓存页和描述信息划分完数据块之后,肯定是会存在一小部分的空间是既不能分配描述信息,又不能放下缓存块的内容的,所以这部分内容没有办法使用。
如何减少呢?
在划分缓存页的时候让他们按照顺序的排列顺序紧密排列,尽可能减少浪费,其实就是顺序的分配内存。
如何知道哪些缓存页是空闲的?
那么mysql是如何知道缓存页是空闲的呢?在Buffer pool 会有一个叫做free list 双向链表,链表每一个节点就是数据页对应的描述数据块。也就是说一个数据页是空闲的,就会放到free链表中,并且在数据库刚刚启动free链表存放了所有描述信息块内容。另外,如果熟悉链表的结构,就会知道链表当中会有一个基础节点(其实就是链表的头指针,只不过内容丰富很多)来存储开始节点和结束节点等内容。
至于这个基础节点的更新操作,熟悉链表的人此时一定十分清楚了,其实就是双向链表的插入操作和删除操作。
为了更好的理解上面一大段的内容,我们通过一个图来包含上面介绍的所有内容:
freelist占用多少内存空间?
Buffer pool 和 freelist里面的数据内容是一模一样的一份拷贝么?**大错特错!**因为描述信息在freelist里面是根据链表的节点规则串联的,同时因为这个节点只需要找到空闲缓存块即可(Free List中的所有节点都会指向一个从未被使用过的缓存页,说白了就是每个节点有个指向空闲缓存页的一对一指针)。
free list 根据链表的定义规则,每一个描述信息都有两个指针,一个是free_pre(前置节点),另一个是free_next(后继节点)。代表了一个双向链表的node,通过这两个指针就把所有的描述数据串联成一个free链表,基础的node节点本身占用了40个字节,存放头节点和尾节点的地址,以及free链表里面当前有多少个节点和其他的信息。
如何将磁盘的页读到buffer pool的缓存页?
如何把磁盘的页读到buffer pool?我们有了free list之后就可以办到了,可以从free链表获取一个描述数据块,接着可以通过这些描述信息找到对应的数据页读到缓冲池里面去,最后再把空闲列表的node去除即可。
如何移除节点?
其实只要通过双向链表的基础节点里面的头尾指针找到节点并且把前置节点或者后置节点的对应引用设置为null即可。也就是熟悉的双向链表的删除操作。
怎么知道数据是否真的进来了?
了解了数据页如何加载到缓冲池,接下来我们来看下mysql怎么知道哪一个数据页加载到缓冲池,一般的流程肯定是当请求进来的时候先检查缓冲池有没有数据,如果没有缓存页就需要先去free list找一下这个数据页的描述信息,然后再通过磁盘文件把这个数据页加载到缓冲池,然后再把free list对应的描述信息节点删除掉。
数据页缓存哈希表的结构是什么?
如果数据页被缓存了会直接查缓存,那么缓冲池怎么知道请求是要找它呢?其实Mysql还有一种结构是哈希表的结构,这个结构可以看作是一个Map,key保存的是表空间+数据页。而value则是缓存页的地址。当请求执行器调用接口的时候,就会根据哈希表找到对应的缓存页,如果没有缓存页就去freelist找到这块数据页加载进来。
脏页
什么是脏页?
当缓冲池的数据被更新,但是磁盘的数据和缓存页的内容不一致的时候,可以说这个页是一个“脏”页。
mysql怎么知道哪些页是脏页
那么mysql是使用free list列表进行确认的?这么做肯定不行,因为这个链表是存放的有哪些数据页没有被加载,并不能知道哪些缓存页是脏页。所以这时候mysql又引入了一个链表,叫做 flush list,它的结构和free list类似也是一个双向链表,同样有一个基础节点维护整个链表的的信息,但是和free list不同的是它所存储的是脏页的描述信息而不是所有的数据页描述信息。(同样也是在每一个节点存在一个指针指向对应的缓存页)
提示:如果还记得本系列的第一篇(前言地址)文章中简单的提到了io线程定期把缓存页刷新到磁盘文件中如何找到脏页的?其实就是通过这个双向链表来实现的,但是刷新的动作是随机刷新。此问题在后续的文章中会再次提到。
最后关于flush list的结构图放到了本文的结尾。
flushlist和freelist伪代码
下面通过两个图来分析一下两个链表的伪代码结构图,关于具体的解释都放到了代码的注释当中,这里就不多啰嗦了:
下面是引入第二个节点之后的内容:
思考题:
逻辑结构和物理结构
我们在SQL语句里都是用到的是表和行的概念,但是之前我 们提到的表空间、数据页,他们之间的关系是什么呢?
表和行是逻辑的存储结构。
但是数据页,表空间,都是物理的存储结构。实际上表的数据都是放在一个表空间,表空间由数据文件(数据块)组成,数据文件存储的是一行行的数据,所以可以认为整个mysql磁盘文件抽象的理解为是由一个个数据块组成的。
物理结构和逻辑结构的区别就是他们的本质区别。
小结:
看完这篇文章相信大家有点晕了,mysql内部两个链表和一个哈希表甚至后续还会有更多的链表来维护信息,这些内容很容易混淆,所以最后再回顾前面提到的结构图来帮助读者进行回顾:
写在最后
缓冲池的内容看似复杂,但是参考结构图理解话其实套路是差不多的,本文的内容难度不会很难,不过到了后续随着缓冲池的细节难度会逐渐加大,最后对于内容编排有任何错误的地方欢迎指出。