1、如何配置你的Buffer Pool的大小?

首先我们来看看,我们应该如何配置你的Buffer Pool到底有多大呢? 因为Buffer Pool本质其实就是数据库的一个内存组件,你可以理解为他就是一片内存数据结构,所以这个内存数据结构肯定是 有一定的大小的,不可能是无限大的。 这个Buffer Pool默认情况下是128MB,还是有一点偏小了,我们实际生产环境下完全可以对Buffer Pool进行调整。 比如我们的数据库如果是16核32G的机器,那么你就可以给Buffer Pool分配个2GB的内存,使用下面的配置就可以了。 [server] innodb_buffer_pool_size = 2147483648 如果有的朋友不知道数据库的配置文件在哪里以及如何修改其中的配置,那建议可以先在网上搜索一些MySQL入门的资料去 看看,其实这都是最基础和简单的。 我们先来看一下下面的图,里面就画了数据库中的Buffer Pool内存组件。
image.png

2、数据页:MySQL中抽象出来的数据单位

接着我们来看下一个问题,假设现在我们的数据库中一定有一片内存区域是Buffer Pool了,那么我们的数据是如何放在Buffer Pool中的? 我们都知道数据库的核心数据模型就是表+字段+行的概念,也就是说我们都知道数据库里有一个一个的表,一个表有很多字 段,然后一个表里有很多行数据,每行数据都有自己的字段值。所以大家觉得我们的数据是一行一行的放在Buffer Pool里面的 吗? 这就明显不是了,实际上MySQL对数据抽象出来了一个数据页的概念,他是把很多行数据放在了一个数据页里,也就是说我 们的磁盘文件中就是会有很多的数据页,每一页数据里放了很多行数据,如下图所示。
image.png
所以实际上假设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页 直接给加载到Buffer Pool里去 也就是说,Buffer Pool中存放的是一个一个的数据页,如下图。
image.png

3、磁盘上的数据页和Buffer Pool中的缓存页是如何对应起来的?

实际上默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容。 而Buffer Pool中存放的一个一个的数据页,我们通常叫做缓存页,因为毕竟Buffer Pool是一个缓冲池,里面的数据都是从磁 盘缓存到内存去的。 而Buffer Pool中默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的,都是16KB。 所以我们看下图,我给图中的Buffer Pool标注出来了他的内存大小,假设他是128MB吧,然后数据页的大小是16KB。
image.png

4、缓存页对应的描述信息是什么?

接着我们要了解下一个概念,对于每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓 存页的 比如包含如下的一些东西:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及别的一些杂七杂 八的东西。 每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool中,每个缓存页的描述数据放在最前面, 然后各个缓存页放在后面 所以此时我们看下面的图,Buffer Pool实际看起来大概长这个样子。
image.png
而且这里我们要注意一点,Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字 节左右的大小,然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130 多MB的样子,因为他里面还要存放每个缓存页的描述数据。

5、数据库启动的时候,是如何初始化Buffer Pool的?

现在我们已经搞明白一件事儿了,那就是数据库的Buffer Pool到底长成个什么样,大家想必都是理解了 其实说白了,里面就是会包含很多个缓存页,同时每个缓存页还有一个描述数据,也可以叫做是控制数据,但是我个人是比较 倾向于叫做描述数据,或者缓存页的元数据,都是可以的。 那么在数据库启动的时候,他是如何初始化Buffer Pool的呢? 其实这个也很简单,数据库只要一启动,就会按照你设置的Buffer Pool大小,稍微再加大一点,去找操作系统申请一块内存区 域,作为Buffer Pool的内存区域。 然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在 Buffer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。 然后当数据库把Buffer Pool划分完毕之后,看起来就是之前我们看到的那张图了,如下图所示。
image.png
只不过这个时候,Buffer Pool中的一个一个的缓存页都是空的,里面什么都没有,要等数据库运行起来之后,当我们要对数据 执行增删改查的操作的时候,才会把数据对应的页从磁盘文件里读取出来,放入Buffer Pool中的缓存页中。

6、free链表-我们怎么知道哪些缓存页是空闲的呢?

接着我们来看下一个问题,当你的数据库运行起来之后,你肯定会不停的执行增删改查的操作,此时就需要不停的从磁盘上读 取一个一个的数据页放入Buffer Pool中的对应的缓存页里去,把数据缓存起来,那么以后就可以对这个数据在内存里执行增删 改查了。 但是此时在从磁盘上读取数据页放入Buffer Pool中的缓存页的时候,必然涉及到一个问题,那就是哪些缓存页是空闲的? 因为默认情况下磁盘上的数据页和缓存页是一 一对应起来的,都是16KB,一个数据页对应一个缓存页。 所以我们必须要知道Buffer Pool中哪些缓存页是空闲的状态。 所以数据库会为Buffer Pool设计一个free链表,他是一个双向链表数据结构,这个free链表里,每个节点就是一个空闲的缓存 页的描述数据块的地址,也就是说,只要你一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中。 刚开始数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有 缓存页的描述数据块,都会被放入这个free链表中 我们看下图所示
image.png
大家可以看到上面出现了一个free链表,这个free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对 应的描述数据块就会加入到这个free链表中,每个节点都会双向链接自己的前后节点,组成一个双向链表。 除此之外,这个free链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节 点,也就是有多少个空闲的缓存页

7、free链表占用多少内存空间?

可能有的人会以为这个描述数据块,在Buffer Pool里有一份,在free链表里也有一份,好像在内存里有两个一模一样的描述数 据块,是么? 其实这么想就大错特错了。 这里要给大家讲明白一点,这个free链表,他本身其实就是由Buffer Pool里的描述数据块组成的,你可以认为是每个描述数据 块里都有两个指针,一个是free_pre,一个是free_next,分别指向自己的上一个free链表的节点,以及下一个free链表的节 点。 通过Buffer Pool中的描述数据块的free_pre和free_next两个指针,就可以把所有的描述数据块串成一个free链表,大家可以 自己去思考一下这个问题。上面为了画图需要,所以把描述数据块单独画了一份出来,表示他们之间的指针引用关系。 对于free链表而言,只有一个基础节点是不属于Buffer Pool的,他是40字节大小的一个节点,里面就存放了free链表的头节点 的地址,尾节点的地址,还有free链表里当前有多少个节点。

8、如何将磁盘上的页读取到Buffer Pool的缓存页中去?

好了,现在我们可以来解答这一篇文章的最后一个问题了,当你需要把磁盘上的数据页读取到Buffer Pool中的缓存页里去的时 候,是怎么做到的? 其实有了free链表之后,这个问题就很简单了。 首先,我们需要从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页,我们看下图 所示。 image.png
接着我们就可以把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述数据写入缓存页的描述数据块里去,比如 这个数据页所属的表空间之类的信息,最后把那个描述数据块从free链表里去除就可以了,如下图所示。
image.png
可能有朋友还是疑惑,这个描述数据块是怎么从free链表里移除的呢?
image.png

9、你怎么知道数据页有没有被缓存?

接着我们来看下一个问题,那你怎么知道一个数据页有没有被缓存呢? 我们在执行增删改查的时候,肯定是先看看这个数据页有没有被缓存,如果没被缓存就走上面的逻辑,从free链表中找到一个 空闲的缓存页,从磁盘上读取数据页写入缓存页,写入描述数据,从free链表中移除这个描述数据块。 但是如果数据页已经被缓存了,那么就会直接使用了。 所以其实数据库还会有一个哈希表数据结构,他会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value。 当你要使用一个数据页的时候,通过“表空间号+数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果 已经有了,就说明数据页已经被缓存了。 我们看下图,又引入了一个数据页缓存哈希表的结构。 也就是说,每次你读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是表空间号+数据页号, value就是缓存页的地址,那么下次如果你再使用这个数据页,就可以从哈希表里直接读取出来他已经被放入一个缓存页了。
image.png

10、flush链表-解决脏数据页刷新到磁盘

1、什么是脏数据页

接着我们看一个很关键的问题,你在执行增删改的时候,如果发现数据页没缓存,那么必然会基于free链表找到一个空闲的缓 存页,然后读取到缓存页里去,但是如果已经缓存了,那么下一次就必然会直接使用缓存页。 反正不管怎么样,你要更新的数据页都会在Buffer Pool的缓存页里,供你在内存中直接执行增删改的操作。 接着你肯定会去更新Buffer Pool的缓存页中的数据,此时一旦你更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据 页里的数据,是不是就不一致了? 这个时候,我们就说缓存页是脏数据,脏页
image.png

2、 哪些缓存页是脏页呢?

其实通过之前的学习,我们都是知道一点的,最终这些在内存里更新的脏页的数据,都是要被刷新回磁盘文件的。 但是这里就有一个问题了,不可能所有的缓存页都刷回磁盘的,因为有的缓存页可能是因为查询的时候被读取到Buffer Pool里 去的,可能根本没修改过! 所以数据库在这里引入了另外一个跟free链表类似的flush链表,这个flush链表本质也是通过缓存页的描述数据块中的两个指 针,让被修改过的缓存页的描述数据块,组成一个双向链表。 凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新 到磁盘上去的 所以flush链表的结构如下图所示,跟free链表几乎是一样的。 image.png

11、引入 LRU算法淘汰-解决Buffer Pool 的缓存页不够问题

1、如果Buffer Pool中的缓存页不够了怎么办?

之前我们已经给大家讲解了Buffer Pool中的缓存页的划分,包括free链表的使用,然后磁盘上的数据页是如何加载到 缓存页里去的,包括对缓存页修改之后,flush链表是如何用来记载脏数据页的。 今天我们接着来分析Buffer Pool的工作原理,我们来思考一个问题,当你要执行CRUD操作的时候,无论是查询数 据,还是修改数据,实际上都会把磁盘上的数据页加载到缓存页里来,这个大家都是没有问题的吧? 那么在加载数据到缓存页的时候,必然是要加载到空闲的缓存页里去的,所以必须要从free链表中找一个空闲的缓存 页,然后把磁盘上的数据页加载到那个空闲的缓存页里去,我们看下图的红色箭头的示意。
image.png
那么大家通过之前的学习肯定都知道了,随着你不停的把磁盘上的数据页加载到空闲的缓存页里去,free链表中的空闲 缓存页是不是会越来越少?因为只要你把一个数据页加载到一个空闲缓存页里去,free链表中就会减少一个空闲缓存 页。 所以,当你不停的把磁盘上的数据页加载到空闲缓存页里去,free链表中不停的移除空闲缓存页,迟早有那么一瞬间, 你会发现free链表中已经没有空闲缓存页了 ,这个时候,当你还要加载数据页到一个空闲缓存页的时候,怎么办呢?如下图。
image.png

2、如果要淘汰掉一些缓存数据,淘汰谁?

针对上述的问题,大家来思考下一个问题,如果所有的缓存页都被塞了数据了,此时无法从磁盘上加载新的数据页到 缓存页里去了,那么此时你只有一个办法,就是淘汰掉一些缓存页。 那什么叫淘汰缓存页呢? 顾名思义,你必须把一个缓存页里被修改过的数据,给他刷到磁盘上的数据页里去,然后这个缓存页就可以清空了, 让他重新变成一个空闲的缓存页。 接着你再把磁盘上你需要的新的数据页加载到这个腾出来的空闲缓存页中去,如下图 image.png 那么下一个问题来了,如果要把一个缓存页里的数据刷入磁盘,腾出来一个空闲缓存页,那么应该把哪个缓存页的数 据给刷入磁盘呢?

3、缓存命中率概念的引入

要解答这个问题,我们就得引入一个缓存命中率的概念。 假设现在有两个缓存页,一个缓存页的数据,经常会被修改和查询,比如在100次请求中,有30次都是在查询和修改 这个缓存页里的数据。那么此时我们可以说这种情况下,缓存命中率很高 为什么呢?因为100次请求中,30次都可以操作缓存,不需要从磁盘加载数据,这个缓存命中率就比较高了。 另外一个缓存页里的数据,就是刚从磁盘加载到缓存页之后,被修改和查询过1次,之后100次请求中没有一次是修改 和查询这个缓存页的数据的,那么此时我们就说缓存命中率有点低,因为大部分请求可能还需要走磁盘查询数据,他 们要操作的数据不在缓存中。 所以针对上述两个缓存页,假设此时让你做一个抉择,要把其中缓存页的数据刷入到磁盘去,腾出来一个空闲的缓存 页,此时你会选择谁? 那还用想么,当然是选择第二个缓存页刷入磁盘中了! 因为第二个缓存页,压根儿就没什么人来使用他里面的数据,结果这些数据还空占据了一个缓存页,这不是占着茅坑 不拉屎么?

4、引入LRU链表来判断哪些缓存页是不常用的

接着我们就要解决下一个问题了,就是你怎么知道哪些缓存页经常被访问,哪些缓存页很少被访问? 此时就要引入一个新的LRU链表了,这个所谓的LRU就是Least Recently Used,最近最少使用的意思。 通过这个LRU链表,我们可以知道哪些缓存页是最近最少被使用的,那么当你缓存页需要腾出来一个刷入磁盘的时 候,不就可以选择那个LRU链表中最近最少被使用的缓存页了么? 这个LRU链表大致是怎么个工作原理呢? 简单来说,我们看下图,假设我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链 表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部 去。
image.png
然后假设某个缓存页的描述数据块本来在LRU链表的尾部,后续你只要查询或者修改了这个缓存页的数据,也要把这 个缓存页挪动到LRU链表的头部去,也就是说最近被访问过的缓存页,一定在LRU链表的头部, 那么这样的话,当你的缓存页没有一个空闲的时候,你是不是要找出来那个最近最少被访问的缓存页去刷入磁盘?此 时你就直接在LRU链表的尾部找到一个缓存页,他一定是最近最少被访问的那个缓存页! 然后你就把LRU链表尾部的那个缓存页刷入磁盘中,然后把你需要的磁盘数据页加载到腾出来的空闲缓存页中就可以 了!

5、简单的LRU链表在Buffer Pool实际运行中,可能导致的一些问题?

1、预读带来的一个巨大问题

但是这样的一个LRU机制在实际运行过程中,是会存在巨大的隐患的。 首先会带来隐患的就是MySQL的预读机制,这个所谓预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可 能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去! 举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓 存里去了,正好每个数据页放入一个空闲缓存页! 但是接下来呢,实际上只有一个缓存页是被访问了,另外一个通过预读机制加载的缓存页,其实并没有人访问,此时 这两个缓存页可都在LRU链表的前面,如下图。
image.png
我们可以看到,这个图里很清晰的表明了,前两个缓存页都是刚加载进来的,但是此时第二个缓存页是通过预读机制 捎带着加载进来的,他也放到了链表的前面,但是他实际没人访问他。

除了第二个缓存页之外,第一个缓存页,以及尾巴上两个缓存页,都是一直有人访问的那种缓存页,只不过上图代表 的是刚刚把头部两个缓存页加载进来的时候的一个LRU链表当时的情况。

这个时候,假如没有空闲缓存页了,那么此时要加载新的数据页了,是不是就要从LRU链表的尾部把所谓的“最近最 少使用的一个缓存页”给拿出来,刷入磁盘,然后腾出来一个空闲缓存页了?

这个时候,如果你把上图中LRU尾部的那个缓存页刷入磁盘然后清空,你觉得合理吗?他可是之前一直频繁被人访问 的啊!只不过在这一个瞬间,被新加载进来的两个缓存页给占据了LRU链表前面的位置,尤其是第二个缓存页,居然 还是通过预读机制加载进来的,根本就不会有人访问!

那么这个时候,你要是把LRU链表尾部的缓存页给刷入磁盘,这是绝对不合理的,最合理的反而是把上图中LRU链表的 第二个通过预读机制加载进来的缓存页给刷入磁盘和清空,毕竟他几乎是没什么人会访问的!

2、哪些情况下会触发MySQL的预读机制?

现在我们已经理解了预读机制一下子把相邻的数据页加载进缓存,放入LRU链表前面的隐患了,预读机制加载进来的 缓存页可能根本不会有人访问,结果他却放在了LRU链表的前面,此时可能会把LRU尾部的那些被频繁访问的缓存页刷 入磁盘中! 所以我们来看看,到底哪些情况下会触发MySQL的预读机制呢?

  1. 有一个参数是innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个 数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓 存里去
  2. 如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会 直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去

    这个机制是通过参数innodb_random_read_ahead来控制的,他默认是OFF,也就是这个规则是关闭的 所以默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓 存页如果一下子都放在LRU链表的前面,而且他们其实并没什么人会访问的话,那就会如上图,导致本来就在缓存里 的一些频繁被访问的缓存页在LRU链表的尾部。 这样的话,一旦要把一些缓存页淘汰掉,刷入磁盘,腾出来空闲缓存页,就会如上所述,把LRU链表尾部一些频繁被 访问的缓存页给刷入磁盘和清空掉了!这是完全不合理的,并不应该这样!

3、全表扫描-另外一种可能导致频繁被访问的缓存页被淘汰的场景

接着我们讲另外一种可能导致频繁被访问的缓存页被淘汰的场景,那就是全表扫描

这个所谓的全表扫描,意思就是类似如下的SQL语句:SELECT * FROM USERS
此时他没加任何一个where条件,会导致他直接一下子把这个表里所有的数据页,都从磁盘加载到Buffer Pool里去。
这个时候他可能会一下子就把这个表的所有数据页都一一装入各个缓存页里去!此时可能LRU链表中排在前面的一大 串缓存页,都是全表扫描加载进来的缓存页!那么如果这次全表扫描过后,后续几乎没用到这个表里的数据呢?
此时LRU链表的尾部,可能全部都是之前一直被频繁访问的那些缓存页! 然后当你要淘汰掉一些缓存页腾出空间的时候,就会把LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下了 之前全表扫描加载进来的大量的不经常访问的缓存页!

4、总结

如果你使用简单的LRU链表的机制,其实是漏洞百出的,因为很可 能预读机制,或者全表扫描的机制,都会一下子把大量未来可能不怎么访问的数据页加载到缓存页里去,然后LRU链 表的前面全部是这些未来可能不怎么会被访问的缓存页! 而真正之前一直频繁被访问的缓存页可能此时都在LRU链表的尾部了! 如果此时此刻,需要把一些缓存页刷入磁盘,腾出空间来加载新的数据页,那么此时就只能把LRU链表尾部那些一直 频繁被访问的缓存页给刷入磁盘了!

12、MySQL基于冷热数据分离的方案,优化LRU算法

1、基于冷热数据分离的思想设计LRU链表

所以为了解决上一讲我们说的简单的LRU链表的问题,真正MySQL在设计LRU链表的时候,采取的实际上是冷热数据 分离的思想。
之前一系列的问题,说白了,不都是因为所有缓存页都混在一个LRU链表里,才导致的么? 所以真正的LRU链表,会被拆分为两个部分,一部分是热数据,一部分是冷数据,这个冷热数据的比例是由 innodb_old_blocks_pct参数控制的,他默认是37,也就是说冷数据占比37%。 这个时候,LRU链表实际上看起来是下面这样子的。
image.png

2、数据页第一次被加载到缓存的时候

好,既然我们知道LRU链表已经按照一定的比例被拆分为了冷热两块区域了,那么接下来就来看看在运行期间,冷热 两个区域是如何使用的。 首先数据页第一次被加载到缓存的时候,这个时候缓存页会被放在LRU链表的哪个位置呢? 实际上这个时候,缓存页会被放在冷数据区域的链表头部,我们看下面的图,也就是第一次把一个数据页加载到缓存 页之后,这个缓存页实际上是被放在下图箭头的位置,也就是冷数据区域的链表头部位置。
image.png

3、冷数据区域的缓存页什么时候会被放入到热数据区域?

接着我们来思考一个问题,第一次被加载了数据的缓存页,都会不停的移动到冷数据区域的链表头部,如上图所示 那么你要知道,冷数据区域的缓存页肯定是会被使用的,那么冷数据区域的缓存页什么时候会放到热数据区域呢? 实际上肯定很多人会想,只要对冷数据区域的缓存页进行了一次访问,就立马把这个缓存页放到热数据区域的头部行 不行呢?如下图所示。
image.png
其实这也是不合理的,如果你刚加载了一个数据页到那个缓存页,他是在冷数据区域的链表头部,然后立马(在1ms 以内)就访问了一下这个缓存页,之后就再也不访问他了呢?难道这种情况你也要把那个缓存页放到热数据区域的头 部吗? 所以MySQL设定了一个规则,他设计了一个innodb_old_blocks_time参数,默认值1000,也就是1000毫秒 也就是说,必须是一个数据页被加载到缓存页之后,在1s之后,你访问这个缓存页,他才会被挪动到热数据区域的链 表头部去。 因为假设你加载了一个数据页到缓存去,然后过了1s之后你还访问了这个缓存页,说明你后续很可能会经常要访问 它,这个时间限制就是1s,因此只有1s后你访问了这个缓存页,他才会给你把缓存页放到热数据区域的链表头部去。 所以我们看下面的图,文字说明做了一点改动,是数据加载到缓存页之后过了1s,你再访问这个缓存页,他就会被放 入热数据区域的链表头部,如果是你数据刚加载到缓存页,在1s内你就访问缓存页,此时他是不会把这个缓存页放入 热数据区域的头部的。
image.png

4、基于冷热分离解决预读和全表扫描等问题

1、对于预读以及全表扫描加载进来的一大堆缓存页

现在我们已经看完了LRU链表的冷热数据分离的方案,那么我们接着看这个冷热数据分离之后的LRU链表,他是如何解 决之前遇到的一大堆问题的? 首先我们思考一下,在这样的一个LRU链表方案下,预读机制以及全表扫描加载进来的一大堆缓存页,他们会放在哪 里? 明显是放在LRU链表的冷数据区域的前面啊! 假设这个时候热数据区域已经有很多被频繁访问的缓存页了,你会发现热数据区域还是存放被频繁访问的缓存页的, 只要热数据区域有缓存页被访问,他还是会被移动到热数据区域的链表头部去。 所以此时你看下图,你会发现,预读机制和全表扫描加载进来的一大堆缓存页,此时都在冷数据区域里,跟热数据区 域里的频繁访问的缓存页,是没关系的! image.png

2、预读机制和全表扫描加载进来的缓存页,能进热数据区域吗?

接着我们看第二个问题,预读机制和全表扫描机制加载进来的缓存页,什么时候能进热数据区域呢? 如果你仅仅是一个全表扫描的查询,此时你肯定是在1s内就把一大堆缓存页加载进来,然后就访问了这些缓存页一 下,通常这些操作1s内就结束了。 所以基于目前的一个机制,可以确定的是,这种情况下,那些缓存页是不会从冷数据区域转移到热数据区域的! 除非你在冷数据区域里的缓存页,在1s之后还被人访问了,那么此时他们就会判定为未来可能会被频繁访问的缓存 页,然后移动到热数据区域的链表头部去!

3、如果此时缓存页不够了,需要淘汰一些缓存,会怎么样?

接着我们看,假设此时缓存页不够了,需要淘汰一些缓存页,此时会怎么做? 那就很简单了,直接就是可以找到LRU链表中的冷数据区域的尾部的缓存页,他们肯定是之前被加载进来的,而且加 载进来1s过后都没人访问过,说明这个缓存页压根儿就没人愿意去访问他!他就是冷数据! 所以此时就直接淘汰冷数据区域的尾部的缓存页,刷入磁盘,就可以了,我们看下图。
image.png

在这样的一套缓存页分冷热数据的加载方案,以及冷数据转化为热数据的时间限制方案,还有就是淘汰缓存页的时候 优先淘汰冷数据区域的方案,基于这套方案,大家会发现,之前发现的问题,完美的被解决了。 因为那种预读机制以及全表扫描机制加载进来的数据页,大部分都会在1s之内访问一下,之后可能就再也不访问了, 所以这种缓存页基本上都会留在冷数据区域里。然后频繁访问的缓存页还是会留在热数据区域里。 当你要淘汰缓存的时候,优先就是会选择冷数据区域的尾部的缓存页,这就是非常合理的了!他不会让刚加载进来的 缓存页占据LRU链表的头部,频繁访问的缓存页在LRU链表的尾部,淘汰的时候淘汰尾部的频繁访问的缓存页了! 问题完美的被解决了。 这就是LRU链表冷热数据分离的一套机制。

5、LRU链表的热数据区域是如何进行优化的?

接着我们来看看LRU链表的热数据区域的一个性能优化的点,就是说,在热数据区域中,如果你访问了一个缓存页, 是不是应该要把他立马移动到热数据区域的链表头部去? 我们看下面的图示。
image.png
但是你要知道,热数据区域里的缓存页可能是经常被访问的,所以这么频繁的进行移动是不是性能也并不是太好?也 没这个必要。 所以说,LRU链表的热数据区域的访问规则被优化了一下,即你只有在热数据区域的后3/4部分的缓存页被访问了,才 会给你移动到链表头部去。 如果你是热数据区域的前面1/4的缓存页被访问,他是不会移动到链表头部去的。 举个例子,假设热数据区域的链表里有100个缓存页,那么排在前面的25个缓存页,他即使被访问了,也不会移动到 链表头部去的。但是对于排在后面的75个缓存页,他只要被访问,就会移动到链表头部去。 这样的话,他就可以尽可能的减少链表中的节点移动了。

6、 对于LRU链表中尾部的缓存页,是如何淘汰他们刷入磁盘的?

1、定时把LRU尾部的部分缓存页刷入磁盘

首先第一个时机,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台 线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入 磁盘里去,清空这几个缓存页,把他们加入回free链表去! 所以实际上在缓存页没用完的时候,可能就会清空一些缓存页了,我们看下面的图示
image.png
所以大家会发现,只要有这个后台线程定时运行,可能你的缓存页都没用完呢,人家就给你把一批冷数据的缓存页刷 入磁盘,清空出来一批缓存页,那么你就多了一批可以使用的空闲缓存页了! 所以如果在一个动态的运行效果中思考,大概就是你不停的加载数据到一些空闲的缓存页里去,然后这些缓存页可能 被使用,会在lru链表中各种移动。然后同时有一个后台线程还不停的把冷数据区域的一些不用的缓存页刷入磁盘中, 清空一些缓存页出来。 只要有缓存页被刷人磁盘,大家可以想象一下,那么这个缓存页必然会加入到free链表中,从flush链表中移除,从lru 链表中移除。

2、把flush链表中的一些缓存页定时刷入磁

如果仅仅是把LRU链表中的冷数据区域的缓存页刷入磁盘,大家觉得够吗? 明显不够啊,因为在lru链表的热数据区域里的很多缓存页可能也会被频繁的修改,难道他们永远都不刷入磁盘中了 吗? 所以这个后台线程同时也会在MySQL不怎么繁忙的时候,找个时间把flush链表中的缓存页都刷入磁盘中,这样被你修 改过的数据,迟早都会刷入磁盘的! 只要flush链表中的一波缓存页被刷入了磁盘,那么这些缓存页也会从flush链表和lru链表中移除,然后加入到free链表 中去! 所以你可以理解为,你一边不停的加载数据到缓存页里去,不停的查询和修改缓存数据,然后free链表中的缓存页不停 的在减少,flush链表中的缓存页不停的在增加,lru链表中的缓存页不停的在增加和移动。 另外一边,你的后台线程不停的在把lru链表的冷数据区域的缓存页以及flush链表的缓存页,刷入磁盘中来清空缓存 页,然后flush链表和lru链表中的缓存页在减少,free链表中的缓存页在增加。 这就是一个动态运行起来的效果!

3、实在没有空闲缓存页了怎么办?

那么实在没有空闲缓存页了怎么办呢? 此时可能所有的free链表都被使用了,然后flush链表中有一大堆被修改过的缓存页,lru链表中有一大堆的缓存页,根 据冷热数据进行了分离,大致是如此的效果。 这个时候如果要从磁盘加载数据页到一个空闲缓存页中,此时就会从LRU链表的冷数据区域的尾部找到一个缓存页, 他一定是最不经常使用的缓存页!然后把他刷入磁盘和清空,然后把数据页加载到这个腾出来的空闲缓存页里去! 这就是MySQL的Buffer Pool缓存机制的一整套运行原理!我们已经完整的讲完了缓存页的加载和使用,以及free链 表、flush链表、lru链表是怎么使用的,包括缓存页是如何刷入磁盘腾出来空闲缓存页的,以及缓存页没有空闲的时候 应该怎么处理。