前面笔者用了两篇文章,讲解InnoDB最核心组件Buffer Pool的部分知识点,对Buffer Pool的内部结构有了一定的了解。
第一讲主要引入了缓存页的概念。
第二讲主要引入了三个链表:free链表、flush链表、lru链表。
现在我们明白,当你执行一个CRUD操作时,InnoDB都会数据从磁盘上的数据页加载到缓存页里来。加载的时候,先从free链表找到一个空闲的缓存页,然后把磁盘上的数据页加载到那个空闲的缓存页里去。如果free链表没有空闲的缓存页了,可以去LRU链表尾部找到最近最少使用的缓存页,把它刷入磁盘,腾出空闲的缓存页,然后加载需要的磁盘数据页到空闲缓存页里去。
有的同学看了文章,可能会觉得lru链表这块,怎么跟我之前看过技术博客讲的不太一样?是不是作者讲错了?
其实不是的,通常分享一项技术,尤其是比较复杂的技术,都是由浅入深的,先介绍简单点的,慢慢介绍更深入的原理。不可能一上来把所有核心知识点一股脑全扔给你。
预读
LRU链表的机制很简单,只要没有空闲缓存页了,就从链表尾部淘汰一些缓存页,把新加载的缓存页放到LRU链表头部。
可是这样的运作机制,会有很大的隐患。
首先就是预读机制!
预读,就是当你从磁盘上加载一个数据页的时候,它可能会连带着把这个数据页相邻的其他数据页,都加载到缓存里去。
设计者这样这样设计,是考虑到当我们访问一个缓存页的时候,很可能会继续访问它相邻的其他缓存页的数据。
但是呢,有时候只有一个缓存页被访问了,其他被加载的缓存页访问一次后,就再也没访问了,此时这些预读的缓存页都在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%。
冷热数据区运作原理
当数据页第一次被加载到Buffer Pool的时候,会被放在冷数据区的头部,如图3所示。
当对冷数据区进行频繁访问后,就会挪到热数据去,这个阈值是由参数innodb_old_blocks_time控制的,其默认值是1000毫秒。
意思就是,一个数据页被加载到缓存页后,在1秒之后,你访问了这个缓存页,它才会被挪动到热数据区的头部。
因为你加载一个数据页到缓存,过了1秒后还会访问这个页,说明你后续会经常访问它,那就放到热数据区吧。如果是1秒内访问,就会判断你以后不会经常访问这个缓存页,也就不会挪到热数据区。
假设现在有一条SQL触发了全表扫描,会加载一大堆缓存页到Buffer Pool,冷热数据分离方案是怎么解决之前的问题的?
这时,预读加载的数据页会放在冷数据区前面。而热数据区不受影响,之前热数据区频繁访问的数据页,现在还可以继续访问。
现在过了1秒种后,如果这些预读加载进来的一大堆缓存页被访问了,那么就会挪动到热数据区头部去。
冷热数据分离思想下,如何淘汰数据?
假设现在缓存页不够了,需要淘汰一些缓存页,怎么办?
直接淘汰冷数据区尾部的缓存页!
因为它们只是被加载进Buffer Pool用了下,1s后就没再使用了,所以可以直接淘汰。
如果你喜欢本文,
请长按二维码,关注 南山的架构笔记
转发至朋友圈,是对我最大的支持。