MySQL 的 Buffer Pool 到底是啥

Buffer Pool 是 InnoDB 在内存里开辟的那块最大的缓存区,用来装从磁盘读上来的数据页和索引页。它是 MySQL 性能的核心——内存装得下,磁盘几乎不用碰;装不下,再快的 CPU 也卡在磁盘 I/O 上。本文按「内存版图 → 为什么需要 → 装了什么 → 内部结构 → LRU → 读写全流程 → 刷脏 → 配置监控」逐层讲透。

database-system/mysql/memory 系列之一 · 与锁、事务、索引专题互相呼应
InnoDB 内存的心脏 页级缓存(16KB/页) 读写都先过它 LRU 冷热分区防污染
1
它在 MySQL 内存版图里的位置

先站远看全局:MySQL 占用内存的,远不止 Buffer Pool 一个。理解它「占多大、和谁并列」,才不会被一知半解带偏。

MySQL 进程占用的内存分两大层:Server 层(各种为单条连接 / 单条语句服务的缓冲)和 存储引擎层 InnoDB。Buffer Pool 是 InnoDB 层里最大、最核心的那块。

图 1 · MySQL 内存版图:Server 层小块缓冲 vs InnoDB 层大块缓冲(Buffer Pool 体量最大)
MySQL 进程占用的内存 ① Server 层(按连接 / 按语句) 线程栈 / 连接缓冲 net_buffer(每连接一份) sort_buffer / join_buffer(排序、连接) read_buffer / read_rnd_buffer(顺序/随机读) binlog_cache(事务 binlog 缓存) 内部临时表(tmp_table_size) ② InnoDB 层(共享、全局) ★ Buffer Pool(最大,本篇主角) 装数据页 / 索引页 / 锁 / 字典 / AHI 通常为物理内存 60%~80% 容量由 innodb_buffer_pool_size 决定 Change Buffer 也寄生在其中 Log Buffer redo 缓冲(16MB) Doublewrite Buffer 双写(防页断裂)
一眼分清三个常混的「缓冲」:
  • Buffer Pool:缓存数据页和索引页(页级),InnoDB 共享,最大头。
  • Log Buffer:缓存redo log 的写入,很小(默认 16MB),和「落盘的数据」是两回事。
  • Query Cache(8.0 已删除):按 SQL 缓存查询结果集,和 Buffer Pool 是不同维度——别再拿它类比。
2
为什么需要 Buffer Pool

一句话:CPU 不能直接算磁盘上的数据,必须先读进内存;而磁盘比内存慢约 10 万倍。

一次查询的本质是:从磁盘把数据读进内存 → 在内存里比较 / 计算 / 排序 → 把结果返回。如果每次都去磁盘读,性能会被 I/O 死死卡住。Buffer Pool 就是那块「内存工作台」——把最常访问的数据页留在内存,尽量让 CPU 只和内存打交道。

延迟量级对比

  • 内存访问:~100 ns
  • SSD 随机读:~100 μs(慢 1000×)
  • 机械盘随机读:~10 ms(慢 10 万×)
  • 网络一次往返:~1 ms 级

没有 Buffer Pool 会怎样

  • 每访问一行都要一次磁盘 I/O
  • 一次全表扫描 = N 次随机读
  • 连接数一上来,磁盘直接被打满
  • CPU 大量时间空等 I/O

有了 Buffer Pool 之后

  • 热点数据常驻内存,逻辑读直接命中
  • 同页多次访问只发生一次物理读
  • 写先改内存 + 记 redo,异步落盘
  • 命中率 99%+ 时,磁盘近乎「隐身」
核心收益:把「随机磁盘 I/O」转成「内存访问 + 顺序写 redo + 后台异步刷盘」。 这是 InnoDB 能扛高并发读写的根本原因,后面 s6/s7 会展开。
3
Buffer Pool 里到底装了什么

它不是「缓存整张表」,而是缓存一个个 16KB 的「页」;里面也不止数据,还有索引、锁、字典等。

InnoDB 中磁盘、内存的最小管理单位都是 页(Page),默认 innodb_page_size = 16KB。Buffer Pool 就是由成千上万个「页帧(buffer frame)」组成的池子,每个帧能装一页。它装的东西:

图 2 · Buffer Pool 的内容构成:数据页 + 索引页占主体,其余是辅助结构
Buffer Pool 数据页 Data Pages 表的实际行记录 占池子最大比例 索引页 Index Pages B+ 树的非叶/叶子节点 查询定位全靠它 其余(约占小头) Change Buffer(非唯一二级索引变更缓存) AHI 自适应哈希索引 / 行锁信息 / 数据字典 Undo 页(部分会被缓存)
两个容易踩的坑:
  • redo log 不在 Buffer Pool 里——它在单独的 Log Buffer(s1 图里已区分)。Buffer Pool 只管「数据 / 索引页」,redo 是「修改日志」,归属不同。
  • Buffer Pool 缓存的是「页」,不是「查询结果」。同一页被查询、被更新都会进池;它不认 SQL 长什么样。
4
内部结构:实例 → 区块 → 页,以及三条链表

大池子被切成多个实例,每个实例由若干 chunk 组成;实例内部用三条链表管理页的状态。

层次(从大到小):

图 3 · 总大小 → 实例 → chunk → 页帧 的层级,以及每个实例自带的三个链表
Buffer Pool 总大小 = innodb_buffer_pool_size 实例 #1(instances 之一) chunk A 若干 16KB 页帧 chunk B 若干 16KB 页帧 Free List(空闲页) LRU List(已用页,冷热分区) Flush List(脏页,待刷盘) 实例 #2 …(共 instances 个) chunk C 若干 16KB 页帧 chunk D 若干 16KB 页帧 Free List LRU List Flush List

Free List

空闲页链表。刚启动、或页被淘汰后,帧回到这里等分配。读入新页时从 Free List 取一帧。

LRU List

正在使用的页(含干净页与脏页)。按「最近最少使用」排序,是淘汰发生的地方——见 s5。

Flush List

所有脏页的链表,按最早被修改的 LSN 排序。后台线程顺着它把脏页刷回磁盘。

注意:一个脏页同时在 LRU List 和 Flush List 里(两个链表指向同一帧,只是管理目的不同)。

5
LRU 的「冷热分区」——最容易被讲错的核心

普通 LRU 会被一次全表扫描冲垮热点数据;InnoDB 用「midpoint 插入 + 老区域 + 1 秒晋升门槛」解决。

普通 LRU 的问题:新页放链表头、命中就提到头、尾部淘汰。听着没问题,但一次全表扫描会把整张表读进池,把原本的热点数据全挤到尾部淘汰掉——结果热点全失,下次访问又得回磁盘。这叫「缓冲池污染」。

InnoDB 的改进(三段式):

图 4 · 改进版 LRU:新页插在 midpoint(old 区头);全表扫描的页在 1 秒内被顺序读完、不满足晋升条件,留在 old 区被淘汰,young 区热点得以保全
young 区(热,约 63%) old 区(冷,约 37%) 热 热 热 热 热 头 新 冷 冷 冷 尾 淘汰 midpoint ↑ 新读入的页插在 old 区头部(不是 young 头) 停留 >1s 且再被访问 → 晋升到 young 全表扫描:1s 内顺序读完 → 不晋升 → 很快被淘汰
为什么这么设计? 全表扫描的页是「一次性」的:读进来后 1 秒之内就被下一页顶掉了,根本没有「再次访问」的机会,因此不满足晋升条件,老老实实待在 old 区被挤出——young 区的真正热点数据毫发无损。这正是 Buffer Pool 抗「临时大查询」的关键。
6
读流程:命中就爽,未命中才碰盘

一次 SELECT 在 Buffer Pool 里走的路。

图 5 · 读路径:先哈希查 buffer frame(O(1));命中=逻辑读直接返回,未命中=从磁盘读入→占 Free 帧→入 LRU
① SQL 要读某页 ② 哈希查 (space,page) → buffer frame 映射表 命中(逻辑读) 直接返回,不碰盘 ✅ 返回客户端 未命中(物理读) ③ 从 Free List 取一帧 ④ 磁盘读入该页 ⑤ 入 LRU(old 区头) 返回数据 磁盘 I/O(慢)
命中率 = 逻辑读 / (逻辑读 + 物理读)。生产环境应长期 > 99%;一旦掉到 95% 以下,基本意味着 Buffer Pool 装不下工作集,要加内存或调大 innodb_buffer_pool_size。
7
写流程:为什么 Buffer Pool 让写也变快

写不立刻落盘,而是「改内存 + 记 redo + 后台异步刷」——把随机写变成顺序写。

一次 UPDATE 的真实路径:

  1. 在 Buffer Pool 里找到目标页(若不在,先按 s6 读入)。
  2. 直接改内存里的页,并把它标记为脏页(dirty)。
  3. 把这次修改顺序写入 redo log(经 Log Buffer)——这是顺序 I/O,极快。
  4. 事务提交时按 innodb_flush_log_at_trx_commit 把 redo 刷盘(保证持久性)。
  5. 异步 脏页由后台 page cleaner 线程稍后刷回数据文件(见 s8)。
  6. 对客户端返回「成功」——此时数据文件里的旧值还在。
图 6 · 写路径:改内存(快)+ 写 redo(顺序,快)→ 立即返回;数据文件落盘是后台异步的(慢,但不阻塞查询)
UPDATE 语句 ① 改 Buffer Pool 页 标记脏页 ② 写 redo(顺序) 经 Log Buffer ③ 返回客户端 ✅ 已提交 ④ 后台 page cleaner 异步刷脏页 按 Flush List 顺序刷回磁盘数据文件 不阻塞查询;崩了靠 redo 重放恢复
关键洞察: 直接往磁盘写是随机写(慢);但 InnoDB 把「随机写」拆成了「内存改(快)+ 顺序写 redo(快)+ 异步随机刷脏(慢但不阻塞)」。崩溃时用 redo 重放即可恢复未刷盘的修改——这叫 WAL(Write-Ahead Logging,预写日志)。
8
脏页什么时候刷回磁盘

「脏」不可怕,关键是控制刷盘时机与节奏,别让脏页堆积或集中爆发。

谁在刷:InnoDB 的后台 page cleaner 线程,按 innodb_io_capacity(默认 200,SSD 可上调)控制每秒刷盘量。

触发刷盘的五种情况:

触发条件说明紧急性
脏页比例超阈值超过 innodb_max_dirty_pages_pct(默认 90%)开始积极刷常态
Free List 不够没空闲帧了,被迫先刷脏页腾位(读请求会卡一下)紧急
redo 快写满checkpoint 推进,必须把对应脏页刷掉才能覆盖旧 redo最紧急
后台定时master 线程约每 1 秒唤醒一次,刷一点常态
正常关闭 / FLUSHshutdown 或 FLUSH TABLES 把所有脏页落盘可控
Checkpoint(检查点)是「redo 能覆盖旧日志」的前提:脏页刷盘后,对应的 redo 段才能被复用。所以「脏页刷得慢」会反压 redo,最终可能拖慢写入——这也是 innodb_io_capacity 要按磁盘能力配好的原因。InnoDB 用的是 fuzzy checkpoint(逐步、分散地推进,而非一次性全刷),避免性能抖动。
9
Change Buffer 与 Buffer Pool 的关系

Change Buffer 不是独立内存,而是 Buffer Pool 里划出的一块,专门缓存「非唯一二级索引的写」。

如果一条 UPDATE 要改一个非唯一二级索引,但对应的索引页不在 Buffer Pool 里:直接去磁盘读这个索引页很贵(随机读)。Change Buffer 的做法是——先把这个改动缓存在 Buffer Pool 里,等以后该索引页被读进池时再合并(merge)进去。

图 7 · Change Buffer 寄生在 Buffer Pool 中:二级索引页不在内存时,写先入 Change Buffer,待该页被读入后再 merge
Buffer Pool Change Buffer(其中一块) 缓存二级索引的写操作 其它数据页 / 索引页 (含该二级索引页被读入后 merge) 二级索引页 此刻在磁盘,不在池 ① 写先入 Change Buffer ② 该页被读入时 merge 回索引
两个要点:
  • 只对「非唯一」二级索引生效——唯一索引每次写都要判重,必须立刻读到页,没法缓。
  • 它位于 Buffer Pool 内部,所以调大 Buffer Pool 也等于给了 Change Buffer 更多空间;innodb_change_buffer_max_size 还能限制它最多占池子的百分比(默认 25%)。
10
生产怎么配、怎么看

给多大、几个实例、怎么看命中率——都是运维高频问题。

配置建议

  • innodb_buffer_pool_size:专用库设为物理内存 60%~80%;混布库留足给 OS 与其它进程。
  • innodb_buffer_pool_instances:池 ≥ 1GB 时默认 8 即可,缓解并发争用。
  • 5.7+ 支持在线扩缩容(靠 chunk 增减),无需重启。
  • innodb_io_capacity:SSD 调到 2000~4000,让刷脏跟上。

怎么看命中率

# 看 Buffer pool hit rate(末尾)
SHOW ENGINE INNODB STATUS\G

# 关键变量
Innodb_buffer_pool_read_requests  # 逻辑读次数
Innodb_buffer_pool_reads          # 物理读(从磁盘)

# 命中率 = 1 - reads / read_requests
# 长期 > 99% 健康;< 95% 多半是内存不够
调参三句话: ① 命中率掉先看内存够不够;② 实例数跟并发走、一般 8 足够;③ 刷脏速度跟磁盘走、SSD 大胆调高 innodb_io_capacity。别一上来就猛加 Buffer Pool,先确认是不是 SQL / 索引问题导致工作集异常大。
11
五个常见误区

把最容易讲错的点一次性讲清。

误区 1:Buffer Pool 是「查询缓存」
错。它缓存页(数据 + 索引),不认 SQL;8.0 的 Query Cache 已删除,两者完全不同维度。
误区 2:redo log 也在 Buffer Pool 里
错。redo 在独立 Log Buffer(默认 16MB),Buffer Pool 只管数据/索引页。
误区 3:设越大越好,越大越快
错。超过物理内存会触发 OS swap,反而更慢;要给 OS 留内存(page cache、连接等)。
误区 4:调大必须重启
错。5.7+ 支持在线 RESIZE(chunk 机制),但大变更会触发一轮页重分布,瞬时略有抖动。
误区 5:脏页 = 危险
错。脏页是正常运行态;怕的是脏页堆积 + 集中刷盘导致抖动,靠 checkpoint 与 io_capacity 平滑即可。
误区 6:全表扫描一定污染池
基本不会。靠 s5 的 old 区 + 1s 晋升门槛,一次性扫描的页进不了 young 区。
12
命中率计算器(自己算一把)

填两组监控值,看 Buffer Pool 是否健康。

逻辑读 read_requests
物理读 reads(磁盘)
填入数值后点「计算命中率」
命中率 = 1 − 物理读 / 逻辑读。
13
总结

一句话收尾

  • 它是什么?Buffer Pool 是 InnoDB 在内存里最大的那块缓存区,按 16KB 页缓存数据页 + 索引页(外加锁、字典、AHI、Change Buffer 等)。
  • 为什么有它?磁盘比内存慢约 10 万倍;CPU 只能算内存里的数据,Buffer Pool 是「内存工作台」,把随机磁盘 I/O 转成内存访问 + 顺序写 redo + 异步刷脏。
  • 内部长啥样?总大小 → 多个实例(默认 8)→ 每个实例多个 chunk(128MB)→ 16KB 页帧;每个实例用 Free / LRU / Flush 三条链表管理。
  • LRU 怎么不污染?分 young/old 区,新页插 midpoint;只有 old 区停留 >1s 且再被访问才晋升——全表扫描冲不垮热点。
  • 读写怎么走?读:哈希查帧、命中即返、未命中才读盘;写:改内存 + 记 redo + 异步刷脏,靠 WAL 保证崩溃可恢复。
  • 怎么配?专用库给物理内存 60%~80%;命中率长期 >99% 健康,掉到 95% 以下多半是内存不够。

速记口诀

  • Buffer Pool = InnoDB 的内存工作台
  • 缓存的是页,不认 SQL
  • redo 在别处,别混进池
  • 新页进 old 区,热点留 young 区

三个易错点

  • 不是查询缓存,是页缓存
  • 脏页正常,堆积才危险
  • 设太大反而 swap,留内存给 OS

关联知识

  • 为什么写要记 redo:事务 / 崩溃恢复专题
  • 页从哪来:索引(B+ 树)专题
  • 页被加锁:locks 系列三篇