专栏文章
专栏文章
MySQL 系列
1. MySQL 系列 #01:MySQL 简介 2. MySQL 系列 #02:MySQL 数据类型与 Java 类型映射 3. MySQL 系列 #03:MySQL 基础架构与执行流程 4. MySQL 系列 #04:MySQL InnoDB 日志系统 5. MySQL 系列 #05:MySQL 事务与 MVCC 6. MySQL 系列 #06:MySQL 索引原理与优化 7. MySQL 系列 #07:MySQL 锁机制 8. MySQL 系列 #08:MySQL 性能问题排查 9. MySQL 系列 #09:MySQL 主备复制与高可用 10. MySQL 系列 #10:MySQL 实战技巧与常见陷阱 11. MySQL 系列 #11:MySQL 数据库设计规范 12. MySQL 系列 #12:MySQL SQL 函数与查询技巧 13. MySQL 系列 #13:MySQL InnoDB Buffer Pool 原理 14. MySQL 系列 #14:MySQL 排序与聚合原理

MySQL 系列 #13:MySQL InnoDB Buffer Pool 原理

发布于 2026-05-26 10:33 👁 11 次阅读
#源码解析#mysql#innodb#buffer-pool#storage

InnoDB Buffer Pool 是 MySQL 最核心的内存结构——所有读写都经过它。本文从整体结构切入,逐层剖析 LRU 管理、预读机制、Change Buffer、脏页刷盘策略,并给出生产配置与监控建议。


目录

章节 说明
整体架构定位 Buffer Pool 在 InnoDB I/O 链路中的位置
内存结构 页类型、控制块、Free List
LRU 管理 young/old 分区、midpoint insertion、防止污染
预读机制 线性预读、随机预读及其触发条件
Change Buffer 写缓冲原理、适用场景、与 Buffer Pool 的关系
脏页管理与刷盘 Flush List、LRU flush、checkpoint、adaptive flushing
多实例 减少锁竞争、分区规则
监控与配置 关键状态变量、生产推荐参数

整体架构定位

innodb buffer pool arch

核心原则:所有数据操作都先在内存(Buffer Pool)中完成,磁盘 I/O 由后台线程异步处理,这是 InnoDB 高性能写入的基础。


内存结构

页(Page)

Buffer Pool 以(默认 16KB)为单位管理内存,页类型包括:

页类型 说明
数据页(index page) 存储行数据,B+ 树叶子节点
索引页 B+ 树非叶子节点
undo 页 回滚段,MVCC 用
Insert Buffer 页 Change Buffer 的存储
自适应哈希索引页 AHI,热点页的哈希索引
锁信息页 行锁位图

控制块(Buffer Block)

每个缓存页对应一个控制块,存放元数据(页的表空间 ID、页号、脏标记、访问时间等),控制块与缓存页 1:1 对应,存储在 Buffer Pool 内存区域的前端。

Free List

Buffer Pool 初始化时,所有页都在 Free List 中。 读取新页时:

  1. 从 Free List 取一个空闲页
  2. 从磁盘加载数据到该页
  3. 从 Free List 移除,加入 LRU List

Free List 耗尽时: 从 LRU List 的 old 区尾部淘汰页(若是脏页则先刷盘)


LRU 管理

经典 LRU 的问题

普通 LRU 遇到全表扫描预读时会将大量冷数据置入链表头部,把真正的热点数据挤出,导致后续查询命中率骤降。

InnoDB 的改进:young/old 分区

innodb lru list

Midpoint Insertion Strategy(中点插入策略)

flowchart LR
    Disk["磁盘加载页"] -->|"插入 midpoint"| Old["old 区头部"]
    Old -->|"停留 > 1000ms 后再次访问"| Young["young 头部"]
    Old -->|"1000ms 内再次访问(预读/全表扫描)"| Old
    Young -->|"LRU 淘汰"| Free["Free List / 刷盘"]

    style Young fill:#cfc,stroke:#060
    style Old fill:#ffc,stroke:#990

young 区优化:减少链表移动

young 区头部 1/4 内的页被访问时不移动(避免频繁修改链表指针带来的 mutex 竞争),只有访问 young 区后 3/4 的页才移到头部。

相关参数

-- old 区占比(默认 37,即 3/8)
innodb_old_blocks_pct = 37

-- 新页在 old 区的最短停留时间(毫秒),超过后才能晋升 young
-- 全表扫描建议调高此值
innodb_old_blocks_time = 1000

预读机制

InnoDB 在判断即将需要某些页时,会提前异步加载它们,减少后续同步 I/O 等待。

线性预读(Linear Read-ahead)

条件:顺序访问同一区(extent,1MB = 64页)内的页数
      超过 innodb_read_ahead_threshold(默认 56)页时
行为:异步预读该区的下一个 extent(64页)
innodb_read_ahead_threshold = 56  -- 触发阈值,0 表示禁用

随机预读(Random Read-ahead)

条件:Buffer Pool 中已有同一 extent 的 13 个页(不要求顺序)
行为:异步加载该 extent 剩余的页
innodb_random_read_ahead = OFF  -- 默认关闭,预测准确率低,建议保持关闭

预读与 LRU 污染

预读的页同样走 midpoint 插入,在 innodb_old_blocks_time 内未被真实访问则留在 old 区,不会污染 young 区热数据。


Change Buffer

是什么

Change Buffer(写缓冲)是 Buffer Pool 中的一块特殊区域,用于缓存对不在 Buffer Pool 中的二级索引页的写操作(INSERT / UPDATE / DELETE)。

写操作流程(无 Change Buffer):
  1. 从磁盘加载二级索引页到 Buffer Pool   ← 随机 I/O,代价高
  2. 修改页
  3. 标记为脏页,异步刷盘

写操作流程(有 Change Buffer):
  1. 检查目标索引页是否在 Buffer Pool
     → 不在:将操作记录到 Change Buffer   ← 顺序写,代价低
     → 在:直接修改页
  2. 后续读取该索引页时,合并 Change Buffer 中的操作

适用条件

Change Buffer 只适用于非唯一二级索引

条件 原因
必须是二级索引(不是主键/聚簇索引) 主键修改必须立即检查唯一性
必须是非唯一索引 唯一索引需要立即从磁盘读取验证唯一性
页不在 Buffer Pool 中 已在内存则直接修改,无需缓冲

Change Buffer 与 Buffer Pool 的关系

flowchart TD
    Write["写非唯一二级索引"] --> Check{"目标页<br/>在 BP 中?"}
    Check -->|"是"| Direct["直接修改页(变脏页)"]
    Check -->|"否"| CB["写入 Change Buffer"]
    CB --> Merge{"合并时机"}
    Merge -->|"读取该页时"| MergeRead["合并到读入的页"]
    Merge -->|"后台线程定期"| MergeBG["后台合并"]
    Merge -->|"数据库关闭时"| MergeShut["全量合并"]

    style CB fill:#ffc,stroke:#990

配置

-- Change Buffer 占 Buffer Pool 的最大比例(默认 25,范围 5-50)
innodb_change_buffer_max_size = 25

-- 缓冲哪些操作(默认 all:inserts + deletes + purges)
innodb_change_buffering = all

生产建议:写多读少(如日志类)的场景可适当调大;读多写少或 SSD 场景可设为 none


脏页管理与刷盘

Flush List

被修改但尚未写回磁盘的页(脏页)在 Flush List 中按 LSN(Log Sequence Number)顺序排列,oldest_lsn 最小的脏页在尾部。

LRU List(按访问时间排序)
  ↕ 同一个页同时存在于两个链表
Flush List(按修改 LSN 排序)

三种刷盘路径

flowchart TD
    A["脏页刷盘触发"] --> B["LRU flush<br/>(BUF_FLUSH_LRU)"]
    A --> C["Checkpoint flush<br/>(BUF_FLUSH_LIST)"]
    A --> D["Adaptive Flushing<br/>(自适应刷新)"]

    B --> B1["LRU 尾部脏页太多<br/>(同步/异步)"]
    C --> C1["redo log 空间不足<br/>推进 checkpoint 释放 log"]
    D --> D1["根据 redo log 增速<br/>动态调整刷新速率"]

    style D fill:#cfc,stroke:#060
刷盘路径 触发条件 影响
LRU flush LRU 尾部脏页比例超过阈值,需要空闲页 可能阻塞用户线程(同步刷)
Checkpoint flush redo log 写满,必须推进 checkpoint 严重时造成写停顿
Adaptive flushing redo log 增速过快,提前刷脏 平滑 I/O,避免突发

Adaptive Flushing 原理

脏页刷新速率由以下因素决定:
1. redo log 使用率(越高 → 刷得越快)
2. Flush List 中脏页的"最老 LSN"距 checkpoint 的距离
3. innodb_io_capacity / innodb_io_capacity_max 设定的 I/O 上限

目标:让 redo log 始终有足够空间,避免到达上限时的强制同步刷新

Double Write Buffer

脏页刷盘前先写入 Double Write Buffer(磁盘上的连续区域),再写入数据文件。防止写入中途断电导致页损坏(partial write)。

innodb_doublewrite = ON  -- 默认开启,SSD 可关闭以提升写性能

多实例

为什么需要多实例

单个 Buffer Pool 有一把全局 mutex,高并发下竞争严重。多实例将 Buffer Pool 拆成独立的 N 份,每份有独立的 LRU/Flush/Free List 和 mutex。

分区规则

页通过 space_id + page_no 哈希到固定的实例,同一页始终在同一实例中。

-- 实例数(建议等于 CPU 核数,最大 64)
innodb_buffer_pool_instances = 8

-- 每个实例大小 = innodb_buffer_pool_size / innodb_buffer_pool_instances
-- 注意:实例数 > 1 时,buffer_pool_size 至少 1GB

innodb_buffer_pool_size < 1GB 时,instances 自动强制为 1。


监控与配置

关键状态变量

-- 查看 Buffer Pool 整体状态
SHOW ENGINE INNODB STATUS\G
-- 关注 BUFFER POOL AND MEMORY 部分

-- 命中率相关
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
变量 说明
Innodb_buffer_pool_read_requests 逻辑读请求总数
Innodb_buffer_pool_reads 从磁盘物理读的次数(未命中)
Innodb_buffer_pool_pages_total 总页数
Innodb_buffer_pool_pages_dirty 当前脏页数
Innodb_buffer_pool_pages_free 空闲页数
Innodb_buffer_pool_wait_free 等待空闲页的次数(>0 说明 BP 压力大)

命中率计算

-- 命中率应尽量接近 100%,低于 95% 需扩大 Buffer Pool
SELECT
  (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100
  AS hit_rate_pct
FROM (
  SELECT
    VARIABLE_VALUE AS Innodb_buffer_pool_reads
  FROM performance_schema.global_status
  WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads'
) r,
(
  SELECT
    VARIABLE_VALUE AS Innodb_buffer_pool_read_requests
  FROM performance_schema.global_status
  WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'
) rr;

生产推荐配置

# Buffer Pool 大小:物理内存的 50%~75%(专用 DB 服务器)
innodb_buffer_pool_size = 8G

# 实例数:CPU 核数,或 buffer_pool_size / 1G,取较小值
innodb_buffer_pool_instances = 8

# I/O 能力(SSD 可设 2000~10000,HDD 约 200)
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000

# Change Buffer:写多读少场景可调大
innodb_change_buffer_max_size = 25

# LRU 预热:重启后自动恢复热点页(MySQL 5.7+)
innodb_buffer_pool_dump_at_shutdown = ON
innodb_buffer_pool_load_at_startup  = ON
innodb_buffer_pool_dump_pct = 25    -- 每个实例保存 LRU 前 25% 的页

# old 区停留时间(全表扫描多的场景可调高,如 3000)
innodb_old_blocks_time = 1000

常见问题排查

现象 可能原因 排查方向
命中率低(< 95%) Buffer Pool 太小 扩大 innodb_buffer_pool_size
wait_free > 0 空闲页不足,脏页来不及刷 检查刷盘速率,调大 innodb_io_capacity
全表扫描后命中率下降 LRU 污染 调大 innodb_old_blocks_time
重启后性能差 Buffer Pool 冷启动 开启 dump/load 预热
写入抖动 redo log 不足或刷盘集中 扩大 innodb_log_file_size,检查 adaptive flushing

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论