⑥ LRU 进化史:从漏洞百出到冷热分离

对应第 16 ~ 20 讲 · 预读与全表扫描的坑 · 冷热分离参数 · 缓存页刷盘时机

简单 LRU 的两大问题(16 讲) 解决方案:冷热数据分离的 LRU(17 ~ 19 讲) 效果验证(18 讲) 缓存页刷盘的时机(20 讲) 问题驱动方案 参数落地 方案验证有效后 问题一 · 预读机制 顺序访问一个区的数据页超过 innodb_read_ahead_threshold(56) → 预读下一区全部页 · 区内 13 连续页频繁访问也会触发(默认 OFF) 预读页没人访问却占 LRU 头部 · 目的:为顺序读提前备货 问题二 · 全表扫描 SELECT * 无 where → 全表数据页一次性装入 LRU 头部 扫完不再访问 → 一直被频繁访问的热页被挤到尾部 淘汰时误杀热数据 · 缓存命中率暴跌 LRU 拆成冷 / 热两段 innodb_old_blocks_pct = 37 → 冷数据区占 37% 新加载页 → 冷数据区头部 预读页 / 全表扫描页都先待在冷区,不进热区 加载 1 秒后再被访问 → 晋升热区头部 innodb_old_blocks_time = 1000ms 热区后 3/4 被访问才移头部(前 1/4 不动,减少链表操作) 刚加载 1ms 内访问一次就再也不来的页 → 留在冷区 预读页 / 全表扫描页 1s 内访问一次后不再访问 → 留在冷区 频繁访问的热页 留在热区 · 被访问移到热区头部 → 冷热隔离互不干扰 需要淘汰时 只淘汰冷区尾部 → 热数据绝不被误杀 ① 后台线程定时刷 LRU 冷区尾部 缓存页没用完就提前清一批 清空后归还 free 链表 未雨绸缪 · 避免 CRUD 现场淘汰 ② MySQL 不繁忙时刷 flush 链表 脏数据迟早要落盘 刷完从 flush / LRU 移除,回 free 热区的脏页也靠这条路径落盘 ③ 实在没有空闲页时 现场淘汰 LRU 冷区尾部 刷盘 → 清空 → 加载新页 频繁发生 = 一次 CRUD 两次磁盘 IO,要靠调参避免 动态效果:一边加载(free ↓ flush/LRU ↑),一边刷盘(flush/LRU ↓ free ↑) 冷热隔离思想同样适用于 Redis 缓存设计:统计热门数据 + 定时任务预加载 图例 问题 机制 / 结论 关键参数 刷盘时机 注解

简单 LRU 为什么会被击穿

  • • 预读:为顺序读提前加载相邻区数据页
  • • 预读页常无人访问,却占据 LRU 头部
  • • 全表扫描一次性灌入全表数据页
  • • 结果:热数据被挤到尾部,淘汰时被误杀

冷热分离方案核心规则

  • • 冷区占 37%:innodb_old_blocks_pct
  • • 新页一律先进冷区头部
  • • 1 秒后再访问才晋升热区:innodb_old_blocks_time
  • • 热区只有后 3/4 的访问才移动头部
  • • 淘汰永远从冷区尾部开始

刷盘三时机与设计思想

  • • 定时刷 LRU 冷区尾部(未雨绸缪)
  • • 空闲时刷 flush 链表脏页
  • • 没有空闲页时现场淘汰冷区尾部
  • • 冷热隔离思想可迁移到 Redis:热门数据预加载