一. 背景
1.1 标准 LRU 算法简介
LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略:当缓存满时,优先淘汰最久未被访问的数据项。其核心思想是"最近用过的数据更可能再次被用到"。
在理想场景下,LRU 表现良好。但在数据库系统中,尤其是面对大规模顺序读操作(如全表扫描)时,标准 LRU 会遭遇严重问题。
1.2 为什么通用 LRU 不适用于数据库?
假设一个 10GB 的 Buffer Pool,执行一次 50GB 表的全表扫描:
- 按标准 LRU,新读入的每一页都会插入到 LRU 头部;
- 扫描过程中,大量"一次性"数据页会不断挤掉热数据;
- 结果 :原本高频访问的热点页被完全驱逐,导致后续查询性能骤降------这就是典型的 "LRU 污染" 或 "全表扫描污染"。
关键洞察 :数据库访问具有 时间局部性 (热点数据反复访问)和 突发性(偶尔的大扫描),标准 LRU 无法区分"临时访问"与"持续热点"。
1.3 标准 LRU vs InnoDB 改进版 LRU
| 特性 | 标准 LRU | InnoDB 改进版 LRU |
|---|---|---|
| 插入位置 | 直接插入头部 | 插入 old sublist 头部(midpoint) |
| 淘汰起点 | 尾部 | old sublist 尾部 |
| 抗扫描污染 | 弱(全表扫描会清空缓存) | 强(新页需二次访问才晋升为 young) |
| 热点识别机制 | 无 | 基于 innodb_old_blocks_time 判断是否"真正热" |
| 结构 | 单一列表 | 分为 young + old 两个子列表 |
二. InnoDB 中 LRU 的具体机制
InnoDB 对标准 LRU 进行了深度改造,核心目标是:保护热点数据不被一次性扫描冲刷掉。
2.1 young / old sublist 结构
InnoDB 将 Buffer Pool 的 LRU 列表逻辑划分为两部分:
- young sublist(热区):存放频繁访问的"真正热"页。
- old sublist(冷区):存放新读入或低频访问的页。
默认情况下,old sublist 占整个 LRU 列表的 3/8(即 37.5%) ,由参数 innodb_old_blocks_pct 控制。
2.2 Midpoint Insertion(中点插入)
- 当从磁盘读入一个新页时,不会直接插入 LRU 头部 ,而是插入到 old sublist 的头部(即整个 LRU 列表的 3/8 位置)。
- 这样,即使发生全表扫描,新页也只在 old 区"排队",不会立即冲击 young 区的热点数据。
2.3 晋升机制(Promotion to Young)
一个页要从 old sublist 晋升到 young sublist,需满足两个条件:
- 在 old sublist 中被再次访问;
- 距离首次插入的时间 ≥
innodb_old_blocks_time(单位:毫秒)。
默认
innodb_old_blocks_time = 1000(1 秒)。这意味着:如果一个页在插入后 1 秒内被再次访问,才被视为"真正热",才会被移到 young 头部。
此举有效过滤掉"短时突发访问"(如扫描中的重复页),只保留持续热点。
2.4 淘汰策略(Eviction)
- 页面淘汰 仅从 old sublist 的尾部开始。
- young sublist 中的页即使长时间未访问,也不会被立即淘汰(除非 old 区为空且仍需空间)。
- 这确保了热点数据的稳定性。
LRU 列表结构示意图
+---------------------------------------------------------------+
| InnoDB LRU List (Buffer Pool) |
+---------------------------+-----------------------------------+
| young sublist | old sublist |
| (hot, frequently used) | (new or infrequently used) |
| | |
| [A] <-> [B] <-> [C] ... | [X] <-> [Y] <-> [Z] ... |
| ^ | ^ ^ |
| | Head (most recent) | | Insert point | Tail (evict)|
+---------------------------+-----------------------------------+
↑
Midpoint (default: 37.5% from tail)
- 新页插入在
[X]位置(old head)。 - 晋升:若
[Y]在 1 秒后被再次访问 → 移至 young head。 - 淘汰:从
[Z](old tail)开始释放。
三. 关键配置参数与调优建议
3.1 核心参数说明
| 参数 | 默认值 | 作用 |
|---|---|---|
innodb_old_blocks_pct |
37 | 控制 old sublist 占 LRU 总长度的百分比(取值 5--95) |
innodb_old_blocks_time |
1000 ms | 页在 old 区需"存活"至少该时间才能晋升 |
innodb_buffer_pool_size |
128M | Buffer Pool 总大小,直接影响 LRU 容量 |
3.2 典型场景调优
场景 1:高并发 OLTP 系统(热点集中)
- 特征:少量表高频访问,极少全表扫描。
- 建议 :
- 降低
innodb_old_blocks_pct(如 20--30),扩大 young 区,提升热点缓存效率。 - 保持
innodb_old_blocks_time=1000,避免噪声干扰。
- 降低
ini
# my.cnf (OLTP 优化)
innodb_buffer_pool_size = 16G
innodb_old_blocks_pct = 25
innodb_old_blocks_time = 1000
场景 2:混合负载(含夜间批处理/报表)
- 特征:白天 OLTP,夜间大查询或 ETL。
- 风险:夜间全表扫描污染 Buffer Pool,影响次日早高峰。
- 建议 :
- 提高
innodb_old_blocks_pct(如 50--60),给扫描数据更多"缓冲空间"。 - 增大
innodb_old_blocks_time(如 2000--5000 ms),确保只有真正重复访问的页才晋升。 - 考虑在批处理前临时调整参数(需动态生效或重启)。
- 提高
ini
# my.cnf (混合负载)
innodb_buffer_pool_size = 32G
innodb_old_blocks_pct = 55
innodb_old_blocks_time = 3000
四. 常见误区与故障排查
4.1 常见误解澄清
| 误区 | 正解 |
|---|---|
| "LRU 列表越长越好" | 列表长度由 innodb_buffer_pool_size 决定,盲目增大可能浪费内存;关键是结构合理而非绝对长度。 |
| "命中率低 = LRU 策略有问题" | 命中率低可能源于:SQL 未走索引、数据倾斜、Buffer Pool 过小、或业务本身访问随机性强,不一定是 LRU 机制缺陷。 |
| "调大 young 区总能提升性能" | 若系统存在大量扫描,过小的 old 区会导致新页频繁 evict,反而增加 I/O。需平衡。 |
4.2 缓冲池命中率低的排查路径
1. 计算命中率:
sql
SHOW ENGINE INNODB STATUS\G
-- 查看 BUFFER POOL AND MEMORY 部分
-- 或使用:
SELECT
(1 - (SUM(IF(variable_name = 'Innodb_buffer_pool_reads', variable_value, 0)) /
SUM(IF(variable_name IN ('Innodb_buffer_pool_read_requests'), variable_value, 0)))) AS hit_rate
FROM performance_schema.global_status;
举例:
sql
SHOW ENGINE INNODB STATUS\G
#得到的部分结果
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 447214
Buffer pool size 8192
Free buffers 1125
Database pages 7053
Old database pages 2619
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 559, not young 14094
0.00 youngs/s, 0.00 non-youngs/s
Pages read 6920, created 8578, written 14900
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 7053, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
计算 InnoDB Buffer Pool 命中率(Hit Ratio),关键在于以下两个指标:
- Pages read:从磁盘物理读取的数据页数量(即未命中缓存的请求)
- Pages read requests:总的逻辑读请求数(包括命中缓存 + 未命中)
但注意:SHOW ENGINE INNODB STATUS 的默认输出中并不直接显示 "Pages read requests" 。
需要从 performance_schema.global_status 中获取该值。
正确计算缓冲池命中率的方法
命中率公式为:
Buffer Pool Hit Rate=(1−Innodb_buffer_pool_readsInnodb_buffer_pool_read_requests)×100% \text{Buffer Pool Hit Rate} = \left(1 - \frac{\text{Innodb\_buffer\_pool\_reads}}{\text{Innodb\_buffer\_pool\_read\_requests}}\right) \times 100\% Buffer Pool Hit Rate=(1−Innodb_buffer_pool_read_requestsInnodb_buffer_pool_reads)×100%
其中:
Innodb_buffer_pool_reads≈ 你看到的 Pages read(实际略有差异,但可近似)Innodb_buffer_pool_read_requests是总逻辑读次数(必须查全局状态)
实际操作步骤
第一步:查询两个关键状态变量
sql
SELECT
VARIABLE_NAME,
VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN
('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests');
假设返回:
Innodb_buffer_pool_read_requests 1000000
Innodb_buffer_pool_reads 20000
第二步:计算命中率
Hit Rate=(1−200001000000)×100%=98% \text{Hit Rate} = \left(1 - \frac{20000}{1000000}\right) \times 100\% = 98\% Hit Rate=(1−100000020000)×100%=98%
📌 经验参考:
- > 99%:优秀(典型 OLTP 场景)
- 95% ~ 99%:可接受
- < 95%:需关注,可能存在索引缺失、全表扫描或 Buffer Pool 过小
为什么 SHOW ENGINE INNODB STATUS 里的 "Pages read" 不够用?
-
Pages read是 自实例启动以来累计从磁盘读入 Buffer Pool 的页数 ,对应Innodb_buffer_pool_reads。 -
但它缺少分母 (总读请求),而分母只能通过
Innodb_buffer_pool_read_requests获得。 -
你贴出的日志中:
Pages read 6920这只是分子,没有分母无法算命中率。
补充:从提供的数据中还能看出什么?
虽然不能直接算命中率,但可以分析 LRU 行为:
- Old database pages: 2619
- Pages made young: 559
- Pages not young: 14094
→ 晋升率 = 559 / (559 + 14094) ≈ 3.8%
这说明:绝大多数新读入的页只被访问了一次(很可能是全表扫描或批处理),没有成为热点。如果你的业务是 OLTP,这个比例偏低,可能需要检查是否有大查询污染 Buffer Pool。
总结
| 问题 | 答案 |
|---|---|
能否仅从 SHOW ENGINE INNODB STATUS 算命中率? |
❌ 不能,缺分母 |
| 正确方法? | ✅ 查 performance_schema.global_status 中的两个变量 |
| 快速 SQL? | 见上文 SELECT ... WHERE VARIABLE_NAME IN (...) |
| 当前数据暗示什么? | 扫描型负载为主,热点页较少 |
2. 判断是否 LRU 相关:
- 检查
Old database pages与Young-making rate(来自INNODB_METRICS或SHOW ENGINE INNODB STATUS)。 - 若
young-making rate极低(< 1%),说明大部分新页未晋升,可能是innodb_old_blocks_time过大或访问模式不适合。 - 若
Pages made young很少,但Pages read很高 → 可能是全表扫描导致 old 区快速淘汰。
3. 工具辅助:
- 启用 Performance Schema 的
memory_summary_global_by_event_name监控 Buffer Pool 使用。 - 使用
sys.innodb_buffer_stats_by_table查看哪些表占用了大量 buffer。
五. 面试题
问题1:为什么 MySQL 要对标准 LRU 算法进行改造?直接使用标准 LRU 会有什么问题?
参考答案 :
标准 LRU 在面对一次性大范围扫描 (如全表查询)时,会将大量临时数据页插入缓存头部,迅速挤掉真正的热点数据,导致缓存污染。
InnoDB 通过引入 old/young 子列表 + midpoint insertion + 晋升延迟机制 ,确保只有被多次访问且间隔合理的页才被视为热点,从而有效隔离扫描流量对核心缓存的冲击,显著提升 OLTP 场景下的缓存稳定性。
问题2:如何通过调整 innodb_old_blocks_pct 优化全表扫描对 Buffer Pool 的影响?
参考答案 :
innodb_old_blocks_pct 控制 old sublist 的占比。
增大该值 (如从默认 37 提高到 50--60)可为全表扫描等一次性读操作提供更大的"缓冲区",避免新页过早进入 young 区或触发频繁淘汰。
配合调高 innodb_old_blocks_time,可进一步确保只有真正重复访问的页才晋升,从而在混合负载下保护热点数据不被冲刷。
问题3:Buffer Pool 命中率突然下降,是否一定说明 LRU 策略有问题?如何系统性排查?
参考答案 :
不一定。命中率下降可能由多种原因引起:
- SQL 层问题:新增未走索引的查询、数据分布变化;
- 容量问题:数据量增长但 Buffer Pool 未扩容;
- 访问模式变化:业务引入大量随机读;
- LRU 配置不当 :如
innodb_old_blocks_time过大导致热点无法晋升。
排查步骤:
- 确认是否伴随 I/O spike 或慢查询增加;
- 检查
SHOW ENGINE INNODB STATUS中的 Buffer Pool 统计(如 young-making rate、old pages evicted); - 使用
sys.schema_table_statistics定位高物理读的表; - 对比变更时间点(如新版本上线、配置修改、数据导入);
- 仅当确认是"扫描污染"或"热点无法保留"时,才调整 LRU 相关参数。