五、Buffer pool 之 LRU 链表

缓存淘汰

  现在我知道数据页要加载到缓存,需要通过 free 链表找到一个空闲的缓存页,然后把数据写入缓存页。但是缓存页的数量是有限的,当缓存页用尽了该咋办呢?应该通过一定的机制把一些缓存页刷回磁盘,空闲一些缓存页出来。那么哪些缓存页需要被刷入磁盘呢?当然是那些不经常使用的缓存页给刷入磁盘啦,这时就需要引入 LRU 链表。

LRU 链表

  与 free 链表类似,LRU 链表也是一个双向链表。元数据从 free 链表中取出,然后对应的缓存页被写入数据,同时元数据加入 LRU 链表。而且只要缓存页被访问(包括查询和修改)就会被移动到链表头部,这样当缓存页不足时,把链表尾部的缓存页刷入磁盘就可以空闲出缓存页了。

数据页频繁淘汰

缓存预读

  当数据页加载到缓存时会连带着把相邻的数据页一同加载到缓存,而不用每次读数据时,都从磁盘加载到缓存,以减少 IO 次数。但是这样一来,很可能预读的缓存根本没有用到,却又占用了缓存页并且处于 LRU 链表头部,而 LRU 尾部的位置可能又使用的频繁。那么恰巧这时缓存页不足需要淘汰时,就会把尾部经常使用的淘汰掉,而预加载的从未使用的却保留了下来。这显然是不合理的。触发预读机制的参数:

  innodb_read_ahead_threshold:默认为 56,如果顺序访问一个数据区的数据页超过这个阈值就会触发预读机制,把下一个数据区的所有数据页都加载到缓存。

  innodb_random_read_ahead:默认为 OFF 关闭的,如果一个数据区连续 13 个数据页被访问,此时会触发预读机制,把这个数据区的所有数据页加载到缓存。

全表扫描

  全表扫描会把表中的数据全部加载到缓存,加入到 LRU 链表的头部,而全表扫描的这些数据可能只会使用一次,但是链表尾部的那些缓存页可能经常使用。淘汰缓存页时就会把那些经常使用的缓存页淘汰掉,而全表扫描的缓存页却保留了下来。

数据冷热分离

  为了解决预加载和全表扫描带来的问题,MySQL 设计了数据冷热分离的 LRU。也就是说实际上 LRU 链表并不是一个单纯的链表,它分为了热数据区和冷数据区,使用 innodb_old_blocks_pct 参数来控制冷数据区占整个链表的比例,默认为 37 。那么数据页首次加载到缓存时,实际上处于冷数据区的缓存页头部。

  MySQL 有个参数 innodb_old_blocks_time 默认值为 1000,在 1000 ms 后再次访问冷数据区头部的缓存页时就会被移动到热数据区的头部了,并不是立即访问就会移动。

  所以这时预加载和全表扫描加载的缓存页会被放在冷数据区,而热数据区的缓存页只要被访问就会一直在热数据区,也就不会导致频繁访问的缓存页被淘汰了。

  基于这样 LUR 冷热分离机制,MySQL 就会优先淘汰冷数据区尾部的缓存页。

  再就是 MySQL 对这套机制进行了优化。按照现有规则会把热数据区访问的数据页移动到头部,如果说热数据区的缓存页使用的极为频繁,那么这种移动将是极为损耗性能的,而且也没有必要。所以对这个规则优化后,只有在热数据区的后面 3/4 的缓存页被访问后会移动到热数据区头部,前面的 1/4 部分不会发生移动,这样就尽可能的减少了链表中的节点移动了。

刷盘

  LRU 尾部的缓存页是如何淘汰刷入磁盘的?首先并不是缓存页耗尽时才会把 LRU 冷数据区尾部的缓存页淘汰刷入磁盘的。MySQL 会有一个定时线程去扫描,每隔一段时间把一些缓存页写入磁盘,留出空闲的缓存页,从 flush 链表移除,加入 free 链表。

  而热数据区也有可能存在被修改的缓存页,这个定时线程会在适当的时机把 flush 中的缓存页都刷入磁盘。被刷入磁盘的缓存页会从 flush 链表和 LRU 链表移除。

  如果实在没有空闲的缓存页,此时又有新的数据页要加载到缓存,那么会从 LRU 链表的冷数据区尾部淘汰刷盘,空闲出缓存页。

posted @   维维尼~  阅读(197)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示