[小技巧35]深入 InnoDB 的 LRU 机制:从原理到调优

一. 背景

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,需满足两个条件:

  1. 在 old sublist 中被再次访问
  2. 距离首次插入的时间 ≥ 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 pagesYoung-making rate(来自 INNODB_METRICSSHOW 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 过大导致热点无法晋升。

排查步骤

  1. 确认是否伴随 I/O spike 或慢查询增加;
  2. 检查 SHOW ENGINE INNODB STATUS 中的 Buffer Pool 统计(如 young-making rate、old pages evicted);
  3. 使用 sys.schema_table_statistics 定位高物理读的表;
  4. 对比变更时间点(如新版本上线、配置修改、数据导入);
  5. 仅当确认是"扫描污染"或"热点无法保留"时,才调整 LRU 相关参数。
相关推荐
独自归家的兔2 小时前
Java性能优化实战:从基础调优到系统效率倍增 -2
java·开发语言·性能优化
独自归家的兔2 小时前
Java性能优化实战:从基础调优到系统效率倍增 - 1
java·开发语言·性能优化
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-考试系统DDD(领域驱动设计)实现步骤详解(2)
java·前端·数据库·人工智能·spring boot
风行無痕2 小时前
MySQL 8.4 数据库修改字段长度的过程
数据库·mysql
難釋懷2 小时前
Redis命令-Hash命令
数据库·redis·哈希算法
難釋懷2 小时前
Redis命令-List命令
数据库·redis·list
zqmattack2 小时前
SQL sever根据身份证判断性别函数
java·数据库·sql
hanqunfeng3 小时前
(七)Redis 命令及数据类型 -- Hash
数据库·redis·哈希算法