MySQL Buffer Pool通过分代LRU(Midpoint Insertion Strategy)隔离预读页与热数据:新页(含预读页)默认插入Old Sublist头部,仅当在Old区停留≥innodb_old_blocks_time且被二次访问时才晋升Young区。

MySQL 的 Buffer Pool 并没有“避免”预读失效,而是承认预读页大概率是冷数据,并通过分代 LRU(Midpoint Insertion Strategy)把它们和热数据隔开——预读失效不是要消灭的现象,而是需要被隔离的访问意图。
预读页为什么一进来就进 Old Sublist
线性预读一次可能加载 64 个页(一个 extent),这些页被顺序读取后基本不会再访问。如果按传统 LRU 直接插到链表头部,就会把高频访问的索引页、主键页一路挤到尾部淘汰。InnoDB 的解法很直接:innodb_old_blocks_pct 默认设为 37%,意味着整个 LRU 链表约 3/8 是 Old Sublist;所有新加载页(含预读页)强制首先进入 Old Sublist 头部,而不是 Young Sublist。
这带来两个实际效果:
- 全表扫描触发的预读页,全部落在 Old 区,彼此之间不干扰 Young 区的热数据排序
- Old 区本身是 FIFO 倾向的:新进来的预读页会把旧的预读页往 Old 尾部推,后者很快被淘汰
- 哪怕
innodb_old_blocks_time被设为 0,只要没被二次访问,页就永远卡在 Old 区,不会晋升
什么情况下预读页能“转正”进 Young Sublist
预读页不是完全没机会留下,但必须满足两个硬条件,缺一不可:
- 该页在 Old Sublist 中存活时间 ≥
innodb_old_blocks_time(默认 1000ms) - 在此期间被再次访问(即第二次 touch)
这意味着:用户登录时反复查 users 表主键页,第一次加载是预读行为(比如走二级索引回表),只要第二次访问间隔超 1 秒,它就会被提拔进 Young Sublist 头部;而全表扫描中只读一次的页,哪怕停留满 1 秒,只要没被再 touch,就一直留在 Old 尾部等淘汰。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
注意:innodb_old_blocks_time 单位是毫秒,不是秒;设成 0 表示跳过时间检查,只看是否被二次访问;设成负数无效,会被重置为默认值。
调高 innodb_read_ahead_threshold 反而更容易污染 Buffer Pool
有人以为“多预读=更高效”,把 innodb_read_ahead_threshold 从默认 56 调到 80 或 100,结果发现 Innodb_buffer_pool_reads 不降反升,QPS 下跌。这是因为:
- 阈值越高,预读越晚触发,但一次拉得更多(比如延迟到第 80 页才批量预读后续几十页)
- 这批集中涌入的页全部塞进 Old Sublist 头部,短时间内大量挤压 Old 区空间
- 如果此时并发有热查询,Old 区里还没来得及被二次访问的“潜在热页”可能被新预读页顶出 Old 尾部,直接淘汰
- 本质上,这是用更大的突发流量加剧了 Old 区的 FIFO 压力,而非提升命中率
监控预读是否真在“失效”要看这两个状态值
别光看 Innodb_buffer_pool_read_requests 涨了多少,关键看物理读有没有同步下降。真正反映预读效率的是:
-
Innodb_buffer_pool_read_ahead:实际触发的预读次数 -
Innodb_buffer_pool_read_ahead_evicted:被预读后从未访问就被淘汰的页数
如果后者长期接近前者(比如比例 > 80%),说明当前 workload 下预读确实大量失效;这时与其关预读,不如检查是否 innodb_old_blocks_pct 过小(导致 Old 区太窄,预读页互相踩踏),或 innodb_old_blocks_time 过大(让本该快速淘汰的页滞留太久)。
最常被忽略的一点:分代 LRU 的效果高度依赖访问模式。OLTP 场景下二次访问间隔短,innodb_old_blocks_time 设太大会卡住热页;而报表类查询若带缓存穿透式扫描,反而需要适当调大该值,防止刚热起来的页又被误判为冷数据踢出。

















