Buffer Pool中的LRU淘汰算法

1,101 阅读7分钟

前言

我们已经了解到buffer pool是通过free链表记载其空闲的缓存页以及flush链表存放等待刷盘的脏页的描述数据块。当我们从磁盘加载数据页到buffer pool的空闲缓存页中,free链表就会移除一个描述数据块。随着不停的增删查改,空闲的缓存页必定越来越少,当某一个瞬间,发现free链表中已经没有空闲的缓存页了,数据库会如何操作呢?

1. 淘汰缓存页

针对buffer pool中没有空闲的缓存页,buffer pool就会淘汰掉一些缓存页。

所谓的淘汰缓存页即是选择被修改过的缓存页,把他们刷入磁盘中,再把缓存页清空,让他们重新成为空闲的缓存页。

图 1

那应该选择哪些缓存页刷入磁盘?

2. 缓存命中率

假设有两个缓存页,一个缓存页的数据经常被修改和查询,比如在100次请求中,有30次都是在查询和修改这个缓存页的数据,就证明缓存命中率很高。

另一个缓存页,从磁盘中加载进来后只修改或则查询过一次,在100次请求中,一次都没有查询或修改过,就证明缓存命中率很低。

对于上述两个缓存页,选择一个刷入磁盘,毫无疑问第二个缓存页才是最佳选择。

3. 判断缓存页是否常用-LRU链表

首先,只要从磁盘中加载数据页到缓存页,就会把这个缓存页的描述数据块放到LRU链表的头部,那么,只要缓存页有数据,他都会在LRU里,而且最近被加载数据的缓存页都会被放到LRU链表的头部。

图2

假设某个缓存页的描述数据块在LRU链表的表尾,后续只要查询或者修改了这个缓存页的数据,就会把这个缓存页的描述数据块移动到LRU链表的头部。即是说,只要最近被访问过的缓存页一定是在LRU链表的头部。

图 3

这时,如果缓存页没有空闲的,就把LRU链表尾部的缓存页刷入磁盘,就可以腾出空闲的缓存页。

4. 简单的LRU链表带来的隐患

MySQL有预读机制,在加载数据页到缓存页的时候,会提前把目标数据页附近的数据页一同加载进去。这样就可能导致一个问题:刚刚加载进来的数据页肯定会放到LRU链表的头部,就算这个缓存页并没有人访问。

图 4

这时刚好要对缓存页进行刷盘,那后面两个缓存页肯定会优先刷入磁盘,而没有人访问的缓存页继续留在磁盘当中,这样变得非常的不合理。如果进行全表扫描的时候,那加载进来没人访问的缓存页会更多,带来的问题会更严重。

MySQL的触发预读机制有两种情况:

  1. 有一个参数是 innodb_read_ahead_threshold , 默认值是56,意思是如果顺序的访问一个区里多个数据页,访问的数据页数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有缓存页都加载到缓存里。
  2. 如果buffer pool里缓存了一个区里的13个连续的数据页,而且这些数据页都比较频繁的被访问,此时就会直接触发预读机制,把这个区里的所有的数据页加载到缓存里去。

第二种预读机制默认是关闭的。

5. 基于冷热数据分离,优化LRU算法

虽然MySQL会把所有加载进来的缓存页放在LRU链表中,但MySQL采取了冷热数据分离的思想。

LRU链表会被拆分成为两部分,一部分为热数据,一部分为冷数据。这个比例是由innodb_old_blocks_pct 参数控制,默认值为37,即是说,冷数据占比 37%。

图 5

1) 数据页第一次加载进来,放在LRU链表的什么地方?

对于这个问题相信很多小伙伴心里都有了答案。放在冷数据区域的头部。

图5-1

2) 冷数据区域的缓存页什么时候放入热数据区域?

我猜很多下同学认为只要对冷数据区域的缓存页进行访问就会把缓存区也放入热数据区域。

其实这个并不合理。假设1秒内对这个缓存页进行了访问就把他放到热数据区域的头部,之后可能也并不会再次对他进行访问,这让也不符合我们的要求。

MySQL设定了一个规则,在 innodb_old_blocks_time 参数中,默认值为1000,也就是1000毫秒。

意味着,只有把数据页加载进缓存里,在经过1s之后再次对此缓存页进行访问才会将缓存页放到LRU链表热数据区域的头部。

图 5-2

为什么是1秒?

因为通过预读机制和全表扫描加载进来的数据页通常是1秒内就加载了很多然后对他们访问一下,这些都是1秒内完成,他们会存放在冷数据区域等待刷盘清空,基本上不太会有机会放入到热数据区域,除非在1秒后还有人访问,说明后续可能还会有人访问,才会放入热数据区域的头部。

3) 淘汰缓存页

看到这里,当buffer pool中缓存页不够的时候,要淘汰缓存页,将缓存页刷盘清空是,冷数据区域尾部的缓存页无疑是最好的淘汰选择。

图 5-3

4) 热数据区域优化

热数据区域中的缓存页会被频繁的访问,每次都将被访问的缓存页放到LRU链表的头部,这样对性能并不友好。

所以对热数据区域的访问规则做了优化,只有对热数据区域的后3/4的缓存页被访问时,才会将缓存页放到头部,即是说,前1/4的缓存页即使被访问,也不会移动。

6. 刷盘时机

对于LRU链表的缓存页刷盘是否需要等待没有空闲的缓存页的时候才进行刷盘?

缓存页刷盘的几个时机:

  • 定时线程执行定时任务,每隔一段时间,将冷数据区域尾部的一些缓存页刷入磁盘,清空缓存页,从LRU链表中移除,加入到free链表当中。
  • flush链表存记录的是被修改过的缓存页,也会被线程随机刷盘清空,从flush链表和lru链表中移除,加入到free链表中。
  • buffer pool中已经没有空闲的缓存页,把冷数据区域尾部的缓存页刷盘清空,腾出空闲的缓存页给将要加载进来的缓存页。

所以,在进行CRUD的时候,形成了一个动态运行的效果。

在不停加载数据页的时候,free链表中的缓存页会不断减少,flush链表中的缓存页也会不断的增加,lru链表中的缓存页也会不断地添加和移动。

另外一边,后台线程也会不断的把lru链表中的冷数据区域的尾部缓存页以及flush链表中的缓存页,刷入磁盘中来清空缓存页。lru链表和flush链表中缓存页在不断减少,free链表中缓存页在不断增加。