Mysql专栏 - 缓冲池的内部结构(二)

前言

这是mysql专栏的第四篇,上一个小节我们了解了如何通过flush list存储所有的脏页数据,这一节我们来继续介绍缓冲池的内部结构LRU链表。

概述

缓冲池的大小是固定的,缓冲池当然不是永远都驻留在缓冲池的,但是空闲缓冲页不够情况下如何处理呢?本节将会讨论缓冲池重要的淘汰机制:LRU的淘汰机制,后续会介绍mysql的冷热数据分离特性,最后将给出几个思考题回顾整个内容。

缓存页的刷新机制 - LRU淘汰缓存页

Buffer pool 中的缓存页不够怎么办?

经过上一节的讨论,当执行器发来了增删改查的请求的时候会从磁盘文件读取对应的数据块到缓冲池当中,之前提到过缓冲池不是无限的,默认情况下最多只有128m,一旦所有的缓存页都被加载就意味着free list内部没有空闲的缓存页,当所有的空闲缓存页被分配完了,这意味着缓冲池已经无法再分配缓冲页了,但是我们还想把数据页加载到缓存池怎么办?

如果我们想要加载新的缓存页也十分简单,只要淘汰一些不常用的缓存页即可。

淘汰那个缓存,淘汰谁?

淘汰缓冲页就是把缓冲池里面的某个缓冲页刷新到磁盘(必须先刷新数据到磁盘)然后把对应的缓存页删除即可。接着再把新的数据页的内容加载到缓冲池即可。那么究竟要把那个缓存页刷新到磁盘呢?

缓存命中率

缓存命中率很好理解,假设有两个缓存页,第一个缓存页在100次请求中查询和修改了30次,意味着这个缓存页的命中率为30%。并且缓存命中率不错。第二个缓存页则在100次内只操作了1,2次,这意味着缓存命中率很低,所以不用说,肯定是淘汰第二个。

LRU链表淘汰算法

为了判断哪些缓存页经常被访问,哪些缓存页很少被访问。mysql引入一个新的LRU链表,LRU 就是least recently used,也就是最少使用的意思。通过这个LRU链表,我们就可以知道那个缓存页是最少使用的,当需要一个新的缓存页的时候就可以通过一个LRU链表知道那个缓存页使用频率最低并将其刷新到磁盘文件并且移除。

当某个缓存页被操作的时候,就会找到LRU列表对应的节点加入进去,需要淘汰一个缓存页,就找到LRU列表的尾部进行淘汰(输入磁盘并且从缓冲池情况,同时free list增加一个空闲的描述信息节点),因为最后一个节点肯定是使用频率最低的。

下面我们根据之前文章的结构图,补充一个LRU链表,最后的结构图内容如下:

Mysql专栏 - 缓冲池的内部结构(二)_后端

简单的LRU链表存在哪些问题?

当Free list没有可用的空闲节点的时候,需要从LRU链表的尾部刷新一个缓存块到磁盘并且清空这个缓存块把位置让给新的数据块。

但是mysql的LRU的链表有许多的特性。那么在介绍新特性之前,我们来看下普通的LRU链表会带来哪些问题

简单的LRU链表有哪些问题呢?

1. 预读:

首先这样的LRU有一个重大的隐患:预读,比如现在存在两个空闲缓存页,加载一个数据页之后,同时会把相邻的数据页页加载到缓存区,正好每一个数据页放入一个空闲缓存页。意味着实际上只有一个缓存页被访问了,另一通过预读的机制加载的缓存页,但是这两个都被放到了链表的最前面,最后,预读会造成尾端的缓存页被错误的删除,然而正确的做法是删除第二个被预读缓存的缓存页

接着我们来看看,到底哪些情况下会触发MySQL的预读机制呢?

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

(2) 如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去

这个预读机制是通过参数**innodb_random_read_ahead**来控制的,他**默认是OFF**,也就是这个规则默认是关闭的


吐槽:预读的机制有点类似机器磁盘的顺序访问操作。


2. 全表扫描

全表扫描相信学过数据库的都知道这个理念,。从底层来看一个全表扫描的查询可能会把表所有的数据页放到buffer pool里面,最终可能会把一整个表的数据页加载到缓存页里面,LRU的前面一大串页都是全表查询的数据页。这会导致尾部淘汰的缓存页是一些经常用到的缓存页,而留下的都是不怎么使用的数据块,这样缓存的命中率会大大降低,导致整个mysql的性能十分差。

冷热数据分离的LRU

解决上面的两个问题激素使用冷热分离的LRU,冷热分离的意思是说按照一定的比例把整个链表分为热数据和冷数据,mysql当中由 innodb_old_blocks_pct 参数进行控制,默认是37, 意味着冷数据占了37%,热数据占了63%

冷热数据如何使用

第一次加载的时候缓存页的数据会放到哪一个位置?稍微琢磨一下不难得出答案那就是:冷数据的头部。第一次把数据页加入到缓存页默认会放到冷数据的头部。

冷数据什么时候进入热数据

冷数据进入热数据肯定是需要一定的缓存命中率的,所以是按照缓存命中率判定的,是这样么?其实不是的这样想是错的,因为这很难作为一个权衡条件。其实冷热数据是按照第一次加载缓冲页1S之后如果你还是访问了这个数据页,那么这个数据就会升级为热数据也就是放到热数据的头部,另外这个参数是根据innodb_old_blocks_time这个参数进行判断的,默认设置的参数就1000(毫秒)也就是1s。

Mysql专栏 - 缓冲池的内部结构(二)_数据库_02

缓存页不够如何淘汰缓存

冷热分离之后淘汰缓存页就简单了,直接找到冷数据的尾部缓存页,把这些缓存文件刷到磁盘文件之后可以直接清除,无需担心他们这些数据可能是频繁访问的数据。

冷热分离如何解决预读和全表查询问题

当预读和全表查询加载出一大堆的数据之后,会发现他们的数据其实都在冷数据的头部的,但是如果1S之后依然频繁访问的冷数据,则会不断的放到热数据的头部去的,但是一大段读取出来的冷数据,由于只访问了一次之后就再也没有访问过了。所以是没有什么关系的。

预读和全表加载的数据,会进入热数据区域么?

如果仅仅是一个全表扫描的查询,此时你肯定是在1s内就把一大堆缓存页加载进来,然后就访问了这些缓存页一下后就完事了,通常这些操作1s内就结束了。也就是说一个全表查的数据许多的临时数据是会直接放到冷数据页的。但是如果这部分数据在1S之后再次被访问,才会升级为热数据。但是“全表查最好尽量避免,错误的热数据也是隐患”。

总结:

到现在为止我们已经彻底搞定了LRU链表的设计机制,刚加载数据的缓存页都是放冷数据区域的头部的,而1s过后被访问了才会放热数据区域的头部,热数据区域的缓存页被访问了,就会自动放到头部去。这样的话实际上冷数据区域放的都是加载进来的缓存页,最多在1s内被访问过,之后就再也没访问过的冷数据缓存页!而加载进来之后在1s过后还经常被访问的缓存页都放在了热数据区域里,他们进行了冷热数据的隔离!这样的话在淘汰缓存的时候,一定是优先淘汰冷数据区域几乎不怎么被访问的缓存页的,最后这种冷热数据分离的思想是十分值得借鉴的一种设计思想。

思考题:

为什么MySQL要设计预读这个机制?

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

为了优化性能引入了预读的机制,顺序读取之后可能会出现后续的顺序读取,所以加载后面的数据页也是合理的,但是理想情况下这种预读可能是好心办坏事,一旦这些预读的页没有加载出来,就是在捣乱了。所以这也是为什么mysql默认情况下是这个规则关闭的(设计的确实不太好)

为什么要设置1S的规则

其实这个规则是针对 全表查询而设置的,因为全表查询会一次性加载出很多的数据页到缓冲池,但是这些数据在短时间可能被误判为热数据,设置1S是因为大部分的全表查询基本都能在1S内完成(当然海量数据除外)。

redis的冷热数据问题

对于这种缓存中同时包含冷热数据的场景,如果你是在Redis中你的业务系统放了很多缓存数据,其中也是冷热数据都有的,此时可能会有什么问题?那么针对这样的一个问题,你是否可以考虑在你自己的缓存设计中,运用冷热隔离的思想来优化重构呢?


肯定是存在问题的,因为假设我们有1亿个商品,然后查询商品不在缓存里面就放到缓存里面,大量不经常访问的数据会在redis里面占用的很多内存但是没有人访问。所以这时候热数据的预加载就会用上的了,统计哪些商品访问的次数最多。然后晚上启动定时任务,把热数据放到redis里面,第二天加载的时候就会优先加载热数据了。


写在最后

如果觉得有帮助希望不忘点个赞给予支持,你的支持和鼓励是我最大的动力,最后欢迎关注个人微信公众号:懒时小窝