一文详解InnoDB最核心组件Buffer Pool(三)

343 阅读7分钟

前面笔者用了两篇文章,讲解InnoDB最核心组件Buffer Pool的部分知识点,对Buffer Pool的内部结构有了一定的了解。

第一讲主要引入了缓存页的概念。

一文详解InnoDB最核心组件Buffer Pool(一)

第二讲主要引入了三个链表:free链表、flush链表、lru链表。

一文详解InnoDB最核心组件Buffer Pool(二)

现在我们明白,当你执行一个CRUD操作时,InnoDB都会数据从磁盘上的数据页加载到缓存页里来。加载的时候,先从free链表找到一个空闲的缓存页,然后把磁盘上的数据页加载到那个空闲的缓存页里去。如果free链表没有空闲的缓存页了,可以去LRU链表尾部找到最近最少使用的缓存页,把它刷入磁盘,腾出空闲的缓存页,然后加载需要的磁盘数据页到空闲缓存页里去。

有的同学看了文章,可能会觉得lru链表这块,怎么跟我之前看过技术博客讲的不太一样?是不是作者讲错了?

其实不是的,通常分享一项技术,尤其是比较复杂的技术,都是由浅入深的,先介绍简单点的,慢慢介绍更深入的原理。不可能一上来把所有核心知识点一股脑全扔给你。

预读

LRU链表的机制很简单,只要没有空闲缓存页了,就从链表尾部淘汰一些缓存页,把新加载的缓存页放到LRU链表头部。

可是这样的运作机制,会有很大的隐患。

首先就是预读机制!

预读,就是当你从磁盘上加载一个数据页的时候,它可能会连带着把这个数据页相邻的其他数据页,都加载到缓存里去。

设计者这样这样设计,是考虑到当我们访问一个缓存页的时候,很可能会继续访问它相邻的其他缓存页的数据。

但是呢,有时候只有一个缓存页被访问了,其他被加载的缓存页访问一次后,就再也没访问了,此时这些预读的缓存页都在LRU链表的最前面。​

图片

                                               图1 LRU链表与预读

​如图1所示,前两个缓存页是刚加载进来的,但第二个缓存页是预读连带加载进来的,它也被放在链表的头部,放在之前加载的缓存页的前面,但并没有人访问它。

此时如果没有空闲缓存页了,就需要从LRU链表尾部淘汰一些缓存页。但是,如果你把图1尾部的两个缓存页清空了,你觉得合理吗?它可是之前一直被访问的缓存页呢,只不过被新加载进来的缓存页给挤到尾部了。

这时候,你要是把LRU链表尾部的缓存页刷入磁盘,肯定是不合理的,反而应该把通过预读加载进来的缓存页刷入磁盘,因为它几乎没人访问。

哪些情况会触发预读机制呢?

(1)innoDB有一个参数,innodb_read_ahead_threshold,它的默认值是56,表示如果按顺序访问一个区里的多个数据页,访问数据页的数量超过了这个值,就会触发预读,把相邻区中的数据页都加载到内存区。

(2)Buffer Pool里缓存了一个区里的13个连续数据页,此时就会触发预读机制。这个机制是通过参数innodb_random_read_ahead来控制的,它默认是关闭的。

所以,一般情况下,只有第一种情况下,会触发预读机制,一下子把很多相邻的数据页加载到缓存去,这些缓存页如果一下子都放在LRU链表的前面,会把本来频繁访问的缓存页放到LRU链表尾部。需要淘汰时,就把这些高访问频率的缓存页淘汰掉。这是不合理的!

其实,最常见触发第一种情况的场景,就是全表扫描。

比如,执行SQL:SELECT * FROM USER。

查询表里的所有数据,会把磁盘里的数据页都加载到Buffer Pool里去,这时LRU链表头部就会有大量的连带加载进来的缓存页,这次SQL查询后,几乎不会再访问到。

如何优化预读?

为了解决上面我们说的预读问题,MySQL在设计LRU链表的时候,并不是简单的Least Recently Userd,而是采用了冷热数据分离的思想

MySQL的LRU链表分为两部分,一部分是热数据,一部分是冷数据,冷热数据的比例由参数innodb_old_blocks_pct控制,默认值37,也就是冷数据默认占37%。​

图片

                                   图2 缓存区冷热数据分离

冷热数据区运作原理

当数据页第一次被加载到Buffer Pool的时候,会被放在冷数据区的头部,如图3所示。​

图片

                                       图3 第一次加载数据的缓存页

当对冷数据区进行频繁访问后,就会挪到热数据去,这个阈值是由参数innodb_old_blocks_time控制的,其默认值是1000毫秒。

意思就是,一个数据页被加载到缓存页后,在1秒之后,你访问了这个缓存页,它才会被挪动到热数据区的头部。

因为你加载一个数据页到缓存,过了1秒后还会访问这个页,说明你后续会经常访问它,那就放到热数据区吧。如果是1秒内访问,就会判断你以后不会经常访问这个缓存页,也就不会挪到热数据区。

​假设现在有一条SQL触发了全表扫描,会加载一大堆缓存页到Buffer Pool,冷热数据分离方案是怎么解决之前的问题的?

这时,预读加载的数据页会放在冷数据区前面。而热数据区不受影响,之前热数据区频繁访问的数据页,现在还可以继续访问。

现在过了1秒种后,如果这些预读加载进来的一大堆缓存页被访问了,那么就会挪动到热数据区头部去。

冷热数据分离思想下,如何淘汰数据?

假设现在缓存页不够了,需要淘汰一些缓存页,怎么办?

直接淘汰冷数据区尾部的缓存页!

因为它们只是被加载进Buffer Pool用了下,1s后就没再使用了,所以可以直接淘汰。

图片

基于缓存页冷热数据分离方案,再加上冷数据转热数据区时间限制,以及优先淘汰冷数据区方案,就完美解决之前的问题了。

预读机制及全表扫描加载的数据页,大部分会在1s内有访问,之后就不会有访问了,所以这种数据基本全留在冷数据区,热数区高访问频率的数据页不受影响。淘汰时,也是淘汰冷数据去尾部的缓存页。

这就是InnoDB中LRU链表的冷热数据分离机制。

InnoDB对LRU链表还有哪些优化?

热数据区是不是只要访问了里边的一个缓存页,就要挪到头部去?

没必要的!要知道热数据区的缓存页访问频率是比较高的。

InnoDB规定,只有处于热数据区后3/4部分的缓存页被访问了,才会挪动到链表头部去。

如果你访问的是热数据区前1/4部分的数据页,就不会挪动到链表头部去。

假设限制热数据区链表长度100,如果你访问的是前面25个缓存页,是不会挪动到链表头部去的,如果你访问的是后75个缓存页,才会挪动到链表头部去。

到这里,基本就把Buffer Pool核心的机制讲完了,此时相信大家看完这三篇文章,脑海里就有一副完整的运作流程图了。

如果你喜欢本文,

请长按二维码,关注 南山的架构笔记