1. 概述
前文提过,Buffer Pool 在使用过程中如果缓存页都使用了,没有空闲的缓存页时,可以去LRU 链表中的尾部找一个最近最少使用的缓存页,把它的数据刷入磁盘,腾出来一个空闲缓存页,然后加载需要的新的磁盘数据页到空闲缓存页里去。
而LRU链表的机制很简单,只要刚从磁盘上加载数据到缓存页里去,这个缓存页就放入LRU 链表的头部,后续如果对任何一个缓存页访问了,也把缓存页从LRU 链表中移动到头部去。这样LRU 链表的尾部,一定是最近最少访问的那个缓存。
2.问题
但是这样会产生两个问题。
其一为了提升性能,MySQL 采用了预读机制,这个所谓的预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去。那就可能会挤掉尾部频繁访问的缓存页。这就不太合理,因为可能通过预读机制加载进来的缓存页根本就没有人访问。
哪些情况下会触发MySQL的预读机制
(1) 有一个参数是innodb_read_ahead_threshold,它的默认值是56,意思是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去。
(2)如果Buffer Pool 里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他数据页都加载到缓存里去
这个机制通过参数innodb_random_read_ahead 来控制的,他默认是OFF, 也就是这个规则是关闭的。
所以默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,如果很少访问,就导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。
其二,全表扫描也很可能导致频繁被访问的缓存页被淘汰的场景,所谓的全表扫描。意思就是类似如下的SQL 语句:SELECT * FROM USERS。但是没加任何一个where 条件,会导致他直接一下子把这个表里所有的数据页,都从磁盘加载到Buffer Pool 里去。这个时候他可能会把一下子就把这个表的所有数据页都一一装入各个缓存页里去。
3. 解决
采用冷热数据分类的思想,真正MySQL 在设计LRU 链表的时候,采取的实际上是冷热分离的思想。
真正的LRU链表,会被拆分两个部分,一部分是热数据,一部分是冷数据,这个冷热数据的比例是由innodb_old_blocks_pct 参数控制的,他默认是37,也就是说冷数据占比37%
首先数据页第一次被加载到缓存的时候,这个时候缓存页会被放在LRU 冷数据区链表的头部位置。
那么冷数据区的缓存页什么时候会被放入到热数据区域。可以设置一个参数 innodn_old_blocks_times
,它的默认值是1000(毫秒)。也就是说,必须是一个数据页被加载到缓存页之后,在1s 之后,你访问这个缓存页,他才会被挪动到数据区域的链表头部去。
这样就可以完美解决之前的问题。因为那种预读机制以及全表扫描机制加载进来的数据页,大部分都会在1s 之内访问一下,之后可能就再也不访问了,所以这种缓存页基本上都会留在冷数据区域里去,然后频繁访问的缓存页还是会留在热数据区域里。当你要淘汰缓存的时候,优先就是会选择冷数据区域的尾部的缓存页。
4. LRU 链表优化
在热数据区域中,如果你访问了一个缓存页,是不是应该要立刻把它移动到热数据区域的链表头部去呢。并不是,只有在热数据区域后的3/4 部分的缓存被访问,才会移动到链表的头部。如果在热数据区域的前面的1/4 的缓存页被访问,他不会移动到链表头部去。
5. 缓存页刷入磁盘的时机
首选第一个时机,并不是缓存页满的时候,才会挑选冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU 链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,把他们加入回free 链表去。
第二,仅仅把LRU链表中的冷数据区域的缓存页刷入磁盘就可以了么。明显不行,因为在lru 链表的热数据区域里的很多缓存页可能也会被频繁的修改,难道他们永远不刷入到磁盘中去么。
后台线程同时也会在MYSQL不怎么繁忙的时候,找个时间把flush 链表中的缓存页都刷入磁盘中,这样被你修改过的数据,迟早都会被刷入磁盘中。
所以你可以理解为,你一边不停的加载数据到缓存页里去,不停的查询和修改缓存数据,然后free 链表中的缓存页不停的在减少,flush 链表中的缓存页不停的在增加,lru 链表中的缓存页不停地在增加个移动。
另外一边,你后台线程不停在把lru 链表的了呢数据区域的缓存页以及flush 链表的缓存页。刷入磁盘中清空缓存页,然后flush 链表和lru 链表中的缓存页在减少,free 链表中的缓存页在增加。
如果实在没有空闲缓存页了怎么办呢,此时可能所有的free链表都被使用了,flush 链表中有一大堆被修改的缓存页,lru 链表中有一大堆的缓存页,根据冷热数据进行了分离,大致如此的效果
这个时候如果要从磁盘加载数据页到一个空闲缓存页中,此时就会从LRU 链表的冷数据区域的尾部找到一个缓存页,他一定是最不常用的缓存页,然后把他刷入磁盘和清空,然后把数据页加载到这个腾出来的空闲缓存页里去的。