MySQL:当buffer pool中的缓存页不够的时候,如何基于LRU算法淘汰部分缓存

LRU链表的引入

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

buffer pool是通过free链表记载其空闲的缓存页以及flush链表存放等待刷盘的脏页的描述数据块。当我们从磁盘加载数据页到buffer pool的空闲缓存页中,free链表就会移除一个描述数据块。

在这里插入图片描述

随着不停的增删查改,空闲的缓存页必定越来越少,当某一个瞬间,发现free链表中已经没有空闲的缓存页了,数据库会如何操作呢?

在这里插入图片描述

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

如果所有的缓存页都被塞了数据,此时无法从磁盘上加载新的数据页到缓存页里去了,此时就只有一个方法:淘汰一些缓存页

那什么叫做淘汰缓存页呢?

  • 就是必须把一个缓存页中被修改过的数据,刷新到磁盘上的数据页中去,然后这个缓存页就可以清空了,让它重新变成一个空闲的缓存页
  • 接着再把磁盘上你需要的新的数据页加载到这个腾出来的空闲缓存页中去

在这里插入图片描述
那么,应该把哪个缓存页的数据给刷入磁盘呢?

缓存命中率概念的引入

要解答这个问题,我们就得引入一个缓存命中率的概念

假设现在有两个缓存页,一个缓存页的数据,经常会被修改和查询,比如在100次请求中,有30次都是在查询和修改
这个缓存页里的数据。那么此时我们可以说这种情况下,缓存命中率很高

为什么呢?因为100次请求中,30次都可以操作缓存,不需要从磁盘加载数据,这个缓存命中率就比较高了。

另一个缓存页,从磁盘加载到缓存页之后,被修改和查询过1次,之后100次请求中没有一次是修改和查询这个缓存页的数据的,那么此时我们就说缓存命中率有点低,因为大部分请求可能还需要走磁盘查询数据,它们要操作的数据不再缓存中。

所以针对上述两个缓存页,假设此时让你做一个抉择,要把其中缓存页的数据刷入到磁盘去,腾出来一个空闲的缓存
页,此时你会选择谁?

那还用想么,当然是选择第二个缓存页刷入磁盘中了!第二个缓存页,压根就没什么人来使用它里面的数据,但是这些数据还是占据了一个缓存页

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

问题是:怎么知道哪些缓存页经常被访问,哪些缓存页很少被访问。

为解决这个问题,Innodb引入了一个LRU链表(这个所谓的LRU就是Least Recently Used,最近最少使用的意思)。

LRU链表的工作原理:

  • 如下图,假设我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,它都会在LRU里了,而且最近被加载数据的缓存页,都会放在LRU链表的头部中去。
    在这里插入图片描述
  • 假设某个缓存页的描述数据块本来在LRU链表的尾部,后继你只要查询或者修改这个缓存页的数据,也要把这个缓存页挪动到LRU链表的头部去。这样在LRU链表的尾部,一定是最近最少被访问的那个缓存页。如下图:

在这里插入图片描述

这样的话,当你的缓存页没有空闲页的时候,直接将LRU链表尾部的缓存页刷入到磁盘,腾出来一个空闲缓存页,然后加载需要的新的磁盘数据页到空闲缓存页里去。

LRU链表实际应用中可能导致的问题

从上面可以可以知道,LRU链表的机制就是,只要是刚从磁盘上加载数据到缓存页里去,这个缓存页就放入到LRU链表的头部,后继如果对任何一个缓存页访问了,就把这个缓存页从LRU链表移动到头部去。

预取带来的问题

但在这样一个LRU机制在实际运行过程中,是会存在巨大隐患的。

首先会带来隐患的是MySQL的预取机制。这个所谓的预取机制,说的就是当你从磁盘上加载一个数据页的时候,它可能会连带着把这个数据页相邻的其他数据页也加载到缓存中去。

举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页!

但是接下来呢,实际上只有一个缓存页被访问了,另一个通过预读机制加载的缓存页,其实并没有人访问,此时这两个缓存页都在LRU链表的前面

在这里插入图片描述
这个时候,加入没有空闲缓存页但是有要加载新的数据页了,基于LRU缓存机制就会把上图中的LRU链表尾部的那个缓存页刷入磁盘然后清空,这合理吗?

这个时候是绝对不合理的,最合理的应该是把上图的LRU链表的第二个通过预读机制加载进来的缓存页给刷入磁盘和和清空,因为它几乎是没什么人会访问的!

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

到底哪些情况下会触发MySQL的预读机制呢?
(1)有一个参数innodb_read_ahead_threshold,它的默认值是56,意思是如何顺序的访问了一个区里的数据的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存中去

(2)如果buffer pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他数据都加载到缓存中去。这个机制是通过参数innodb_random_read_ahead来控制的,他默认是OFF,也就是这个规则是关闭的

所以默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,而且他们其实并没什么人会访问的话,那就会如上图,导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。

这样的话,一旦要把一些缓存页淘汰掉,刷入磁盘,腾出来空闲缓存页,就会如上所述,把LRU链表尾部一些频繁被访问的缓存页给刷入磁盘和清空掉了!这是完全不合理的,并不应该这样!

另一种可能导致频繁被访问的缓存页被淘汰的场景

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

这个所谓的全表扫描,意思就是类似如下的SQL语句:SELECT * FROM USERS。

此时它没有任何where条件,会导致一下子把这个表里面的所有的数据页,都从磁盘加载到buffer pool里去。

这个时候可能LRU链表中排在前面的一大串缓存页,都是全表扫描加载进来的缓存页。那么如果这次全表扫描之后,后继几乎没用到这个表里的数据呢?

此时LRU链表的尾部,可能全部都是之前一直被频繁访问的那些缓存页!

然后当你要淘汰掉一些缓存页腾出空间的时候,就会把LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下了之前全表扫描加载进来的大量的不经常访问的缓存页!

问题

为什么MySQL要设计预读这个机制?加载一个数据页到缓存里去的时候,为什么要把一些相邻的数据页也加载到缓存里去呢?这么做的意义在哪里?是为了应对什么样的一个场景?

为了优化性能,MySQL才设计了预读机制,也就是说如果在一个区内,你顺序读取了好多数据页了,比如数据页01~数据页56都被你依次顺序读取了,MySQL会判断,你可能接着会继续顺序读取后面的数据页。

因此它就干脆提前把后续的一大堆数据页(比如数据页57~数据页72)都读取到Buffer Pool里去,那么后续你再读取数据页60的时候,是不是就可以直接从Buffer Pool里拿到数据了?

当然理想是上述那样,很丰满,但是现实可能很骨感。你预读的一大堆数据页要是占据了LRU链表的前面部分,可能这些预读的数据页压根儿后续没人会使用,那你这个预读机制就是在捣乱了。

MySQL是如何基于冷热数据分离的方案,来优化LRU算法的

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

真正MySQL在设计LRU链表的时候,采取的实际上是冷热数据分离的思想。

之前一系列的问题,说白了,不都是因为所有缓存页都混在一个LRU链表里,才导致的么?

所以真正的LRU链表,会被拆分为两个部分,一部分是冷数据,一部分是热数据。这个冷热数据的比例是由innodb_old_blocks_pct参数控制的,默认为37,也就是冷数据占比37%

在这里插入图片描述

问题是在运行期间,冷热两个区域是如何使用的呢?

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

问题:数据页第一次被加载到缓存的时候,这个时候缓存页会被放在链表的哪个位置呢?

实际上这个时候,缓存页会被放在冷数据区域的链表头部。如下图:

在这里插入图片描述

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

第一次被加载了数据的缓存页,都会不停的移动到冷数据区域的链表头部。冷数据区域的缓存页肯定是会被使用的,那么冷数据区域的缓存页什么时候会被放在热数据区域呢?是不是只要对冷数据区域的缓存页进行了一次访问,就立马把这个缓存页放到热数据区域的头部呢?

这肯定不行的。MySQL设计了一个innodb_old_blocks_time参数,默认值1000,也就是1000毫秒。也就是说必须是一个数据页被加载到缓存页之后,在1s之后,你访问这个缓存页,它才会被挪到热数据区域的链表头部去。

因为假设你加载了一个数据页到缓存中去,然后过了1s之后你还访问了这个缓存页,说明你后继很可能会经常要访问它,因此只有1s后你访问了这个缓存页,他才会给你把缓存页放到热数据区域的链表头部去。

在这里插入图片描述

问题:在这样的一个LRU链表方案下,预读机制以及全表扫描加载进来的一大堆缓存页,他们会放在哪里?

明显是放在LRU链表的冷数据区域的前面啊
在这里插入图片描述

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

如果你仅仅是一个全表扫描的查询,此时你肯定是在1s内就把一大堆缓存页加载进来,然后就访问了这些缓存页一下,通常这些操作1s内就结束了。

所以基于目前的一个机制,可以确定的是,这种情况下,那些缓存页是不会从冷数据区域转移到热数据区域的!

除非你在冷数据区域里的缓存页,在1s之后还被人访问了,那么此时他们就会判定为未来可能会被频繁访问的缓存页,然后移动到热数据区域的链表头部去!

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

直接淘汰冷数据区域的尾部的缓存页,刷入磁盘

在这里插入图片描述

问题:在LRU链表的冷数据区域中的都是什么样的数据呢?

大部分应该都是预读加载进来的缓存页,加载进来1s之后都没人访问的,然后包括全表扫描或者一些大的查询语句,加载一堆数据到缓存页,结果都是1s之内访问了一下,后继就不再访问这些表的数据了

问题:redis会不会有冷热数据的问题?如果有,怎么解决呢?

有。常见的一个场景就是电商系统里的商品缓存数据,假设你有1亿个商品,然后只要查询商品的时候发现商品不在缓存里,就给他放到缓存里去,你要这么搞的话,必然导致大量的不怎么经常访问的商品会被放在Redis缓存里!

经常被访问的商品其实就是热数据,不经常被访问的商品其实就是冷数据,我们应该尽量让Redis里放的都是经常访问的热数据,而不是大量的冷数据。因为你放一大堆不怎么经常访问的商品在Redis里,那么他占用了很多内存,而且后续还不怎么会访问到他们!

所以我们在设计缓存的时候,可以采用热数据的缓存预加载。也就是说,每天统计出来哪些商品被访问的次数最多,然后晚上的时候,启动一个定时作业,把这些热门商品的数据,预加载的redis里。第二天对热门商品的访问就自然会优先走Redis缓存了

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

问题:在热数据区域中,如果你访问了一个缓存页,是不是应该把它立马移动到热数据区域的链表头部去?

在这里插入图片描述
不是,热数据区域里的缓存页可能是经常被访问的,这么频繁的进行移动性能就不会太好。

所以,LRU链表的热数据区域的访问规则被优化了一下:只有在热数据区域的后3/4部分的缓存页被访问到了,才会移动到链表头部去。

对于LRU链表中尾部的缓存页,刷盘的时机

buffer pool在运行中被使用的时候,会频繁的从磁盘上加载数据页到它的缓存页中去,然后free链表、fflush链表、lur链表都会在使用的时候同时被使用。

  • 比如数据加载到一个缓存页,free链表里会移除这个缓存页,然后lru链表的冷数据区域的头部会放入这个缓存页
  • 比如修改了一个缓存页,那么flush链表就会记录这个脏页,lru链表还可能把这个缓存页从冷数据区域移动到热数据区域的头部去
  • 比如你查询了一个缓存页,那么此时就会把这个缓存页在lru链表中移动到热数据区域中去,或者在热数据区域中也有可能移动到头部去

总之,MySQL在执行CRUD的时候,首先就是大量的操作缓存页以及对应的几个链表,然后在缓存页都满的时候,必须想办法把一些缓存页给刷入磁盘,然后清空这几个缓存页,接着把需要的数据页加载到缓存页中去。

数据库是根据LRU链表去淘汰缓存页的,那么它到底是什么时候把LRU链表的冷数据区域中的缓存页刷入磁盘的呢?实际上它有几个时机

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

首先第一个时机,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,把他们加入回free链表去!

在这里插入图片描述

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

这个后台线程同时也会在MySQL不怎么繁忙的时候,找个时间把fflush链表中的缓存页都刷入磁盘中,这样被修改过的数据,迟早都会刷入磁盘的。

只要fflush链表中的一波缓存页被刷入了磁盘,那么这些缓存页也会从fflush链表和lru链表中移除,然后加入到free链表中去。

即:

  • 一边不停的加载数据到缓存页中去,不停的查询和修改缓存数据,然后free链表中的缓存页不停的在减少,flush链表中的缓存页不停的在增加,lru链表中的缓存页不停的在增加和移动。
  • 另外一边,后台线程不停的在把lru链表的冷数据区域的缓存页以及flush链表的缓存页,刷入磁盘中来清空缓存页,然后flush链表和lru链表中的缓存页在减少,free链表中的缓存页在增加。

这就是一个动态运行起来的效果!

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

此时可能所有的free链表都被使用了,然后fflush链表中有一大堆被修改过的缓存页,lru链表中有一大堆的缓存页,根据冷热数据进行了分离。

这个时候如果要从磁盘加载数据页到一个空闲缓存页中,此时就会从LRU链表的冷数据区域的尾部找到一个缓存页,然后把他刷入磁盘和清空,然后把数据页加载到这个腾出来的空闲缓存页里去

这个就是MySQL的Buffer Pool缓存机制的一整套运行原理

但是如果频繁的出现这样的一个情况,那你的很多CRUD执行的时候,难道都要先刷一个缓存页到磁盘上去?然后再从磁盘上读取一个数据页到空闲的缓存页里来?这样岂不是每次CRUD操作都要执行两次磁盘IO?那么性能岂不是会极差?

所以问题是MySQL的内核参数,应该如何优化,优化哪些地方的行为,才能够尽可能的避免在执行CRUD的时候,经常要先刷一个缓存页到磁盘上去,才能读取一个磁盘上的数据页到空闲缓存页里来?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值