1. LRU链表在Buffer Pool的应用
Buffer Pool在使用过程中如果缓存页都使用了,没有空闲的缓存页时,可以去LRU链表中的尾部找一个最近最少使用的缓存页,把它的数据刷入磁盘,腾出来一个空闲缓存页,然后加载需要的新的磁盘数据页到空闲缓存页里去。
LRU链表的机制,只要是刚从磁盘上加载数据到缓存页里去,这个缓存页就放入LRU链表的头部,后如果对任何一个缓存页的访问,都会把缓存页从LRU链表中移动到头部去。
这样在LRU链表的尾部,一定是最近最少被访问的那个缓存页。
2.LRU机制在实际运行中存在的隐患
首先会带来的隐患是MySQL的预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可能会把这个数据页相邻的其他数据页,也加载到缓存页里去。
举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把它的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页。
实际上这里只有一个缓存页是被访问了的,另外一个通过预读机制加载的缓存页,并没有人访问,但由于LRU机制导致两个缓存页都在LRU链表的前面。
参照上图,假如没有空闲缓存页了,那么此时要加载新的数据页了,就需要从LRU链表的尾部把所谓的“最近最少使用的一个缓存页” 给拿出来,刷入磁盘。
这时候有个问题,那就是如果把上图中LRU尾部的那个缓存页刷入磁盘然后清空,那这是不合理的,因为它之前一直频繁被人访问,只不过在这一瞬间,被新加载进来的两个缓存页给占据了LRU链表前面的位置,尤其是第二个缓存页,它是通过预读机制加载进来的,并没有人去访问。
最合理的应该是把上图中LRU链表的第二个通过预读机制加载进来的缓存页给刷入磁盘和清空,因为它几乎没有被访问过。
3.触发MySQL预读机制的场景
触发MySQL预读机制的情况有以下几种:
(1)有一个参数是 innodb_read_ahead_threshold,它的默认值是56,意思是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去。
(2)如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去。
预读机制是通过参数 innodb_random_read_ahead 来控制的,它默认是OFF,也就是说这个规则是默认关闭的。
默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,而且没有什么人访问的话,就会导致原来在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。
从而导致这部分频繁被访问的缓存页给刷入磁盘和清空掉。
4. 全表扫描导致频繁被访问的缓存页被淘汰的场景
另外一种可能导致频繁被访问的缓存页被淘汰的场景,那就是全表扫描。
所谓全表扫描,就是类似如下的SQL语句: SELECT * FROM USERS。这种语句没加任何一个where 条件,导致一下子把该表中所有的数据页,都从磁盘加载到Buffer Pool里去。
在全表扫描的场景下,会一下子把这个表的所有数据页都一一装入各个缓存页里去。此时可能LRU链表中排在前面的一大串缓存页,都是全表扫描加载进来的数据页。
这就会导致这次全表扫描过后,排在LRU链表头部的是几乎没用到的表里的数据的缓存页。
而在LRU链表的尾部,可能全都是之前一直被频繁访问的那些缓存页。
然后当你要淘汰一些缓存页腾出空间时,就会把LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下之前全表扫描加载进来的大量的不经常访问的缓存页。
5.总结
在使用简单的LRU链表机制时,由于预读机制或全表扫描的机制,会导致一下子把大量未来可能不怎么访问的数据页加载到缓存里去,然后LRU链表的前面全部是这些未来可能不怎么被访问的缓存页。
而真正之前一直被频繁访问的缓存页可能都在LRU链表的尾部。
如果没有空闲缓存页的时候,就会导致把LRU链表尾部那部分频繁被访问的缓存页刷入磁盘、清空掉,以此来腾出空间。