MySQL 的 Buffer Pool 到底是啥
Buffer Pool 是 InnoDB 在内存里开辟的那块最大的缓存区,用来装从磁盘读上来的数据页和索引页。它是 MySQL 性能的核心——内存装得下,磁盘几乎不用碰;装不下,再快的 CPU 也卡在磁盘 I/O 上。本文按「内存版图 → 为什么需要 → 装了什么 → 内部结构 → LRU → 读写全流程 → 刷脏 → 配置监控」逐层讲透。
database-system/mysql/memory 系列之一 · 与锁、事务、索引专题互相呼应
InnoDB 内存的心脏
页级缓存(16KB/页)
读写都先过它
LRU 冷热分区防污染
先站远看全局:MySQL 占用内存的,远不止 Buffer Pool 一个。理解它「占多大、和谁并列」,才不会被一知半解带偏。
MySQL 进程占用的内存分两大层:Server 层(各种为单条连接 / 单条语句服务的缓冲)和 存储引擎层 InnoDB。Buffer Pool 是 InnoDB 层里最大、最核心的那块。
图 1 · MySQL 内存版图:Server 层小块缓冲 vs InnoDB 层大块缓冲(Buffer Pool 体量最大)
一眼分清三个常混的「缓冲」:
- Buffer Pool:缓存数据页和索引页(页级),InnoDB 共享,最大头。
- Log Buffer:缓存redo log 的写入,很小(默认 16MB),和「落盘的数据」是两回事。
- Query Cache(8.0 已删除):按 SQL 缓存查询结果集,和 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 会展开。
它不是「缓存整张表」,而是缓存一个个 16KB 的「页」;里面也不止数据,还有索引、锁、字典等。
InnoDB 中磁盘、内存的最小管理单位都是 页(Page),默认 innodb_page_size = 16KB。Buffer Pool 就是由成千上万个「页帧(buffer frame)」组成的池子,每个帧能装一页。它装的东西:
图 2 · Buffer Pool 的内容构成:数据页 + 索引页占主体,其余是辅助结构
两个容易踩的坑:
- redo log 不在 Buffer Pool 里——它在单独的 Log Buffer(s1 图里已区分)。Buffer Pool 只管「数据 / 索引页」,redo 是「修改日志」,归属不同。
- Buffer Pool 缓存的是「页」,不是「查询结果」。同一页被查询、被更新都会进池;它不认 SQL 长什么样。
4
内部结构:实例 → 区块 → 页,以及三条链表
大池子被切成多个实例,每个实例由若干 chunk 组成;实例内部用三条链表管理页的状态。
层次(从大到小):
- Buffer Pool 总大小:
innodb_buffer_pool_size(默认 128MB,5.7 起上调)。
- 实例(Instance):总大小按
innodb_buffer_pool_instances 切分(默认 8,当总大小 ≥ 1GB 时;≤1GB 时默认 1)。作用 减少多个线程抢同一把大锁的竞争,每个实例独立管理自己的链表。
- 区块(Chunk):每个实例由若干 chunk 组成,
innodb_buffer_pool_chunk_size 默认 128MB。在线扩缩容就靠「增减 chunk」实现(5.7+)。
- 页(Page):chunk 里是一个个 16KB 的页帧。
图 3 · 总大小 → 实例 → chunk → 页帧 的层级,以及每个实例自带的三个链表
Free List
空闲页链表。刚启动、或页被淘汰后,帧回到这里等分配。读入新页时从 Free List 取一帧。
LRU List
正在使用的页(含干净页与脏页)。按「最近最少使用」排序,是淘汰发生的地方——见 s5。
Flush List
所有脏页的链表,按最早被修改的 LSN 排序。后台线程顺着它把脏页刷回磁盘。
注意:一个脏页同时在 LRU List 和 Flush List 里(两个链表指向同一帧,只是管理目的不同)。
普通 LRU 会被一次全表扫描冲垮热点数据;InnoDB 用「midpoint 插入 + 老区域 + 1 秒晋升门槛」解决。
普通 LRU 的问题:新页放链表头、命中就提到头、尾部淘汰。听着没问题,但一次全表扫描会把整张表读进池,把原本的热点数据全挤到尾部淘汰掉——结果热点全失,下次访问又得回磁盘。这叫「缓冲池污染」。
InnoDB 的改进(三段式):
- LRU 被分成 young 区(新 / 热) 和 old 区(老 / 冷),old 区默认占 37%(
innodb_old_blocks_pct)。
- 新读入的页不放在链表头,而是插在冷热分界点(midpoint),即 old 区的头部。
- 只有页在 old 区停留超过
innodb_old_blocks_time(默认 1000 ms)后再次被访问,才晋升到 young 区。
图 4 · 改进版 LRU:新页插在 midpoint(old 区头);全表扫描的页在 1 秒内被顺序读完、不满足晋升条件,留在 old 区被淘汰,young 区热点得以保全
为什么这么设计? 全表扫描的页是「一次性」的:读进来后 1 秒之内就被下一页顶掉了,根本没有「再次访问」的机会,因此不满足晋升条件,老老实实待在 old 区被挤出——young 区的真正热点数据毫发无损。这正是 Buffer Pool 抗「临时大查询」的关键。
一次 SELECT 在 Buffer Pool 里走的路。
图 5 · 读路径:先哈希查 buffer frame(O(1));命中=逻辑读直接返回,未命中=从磁盘读入→占 Free 帧→入 LRU
命中率 = 逻辑读 / (逻辑读 + 物理读)。生产环境应长期 > 99%;一旦掉到 95% 以下,基本意味着 Buffer Pool 装不下工作集,要加内存或调大 innodb_buffer_pool_size。
7
写流程:为什么 Buffer Pool 让写也变快
写不立刻落盘,而是「改内存 + 记 redo + 后台异步刷」——把随机写变成顺序写。
一次 UPDATE 的真实路径:
- 在 Buffer Pool 里找到目标页(若不在,先按 s6 读入)。
- 直接改内存里的页,并把它标记为脏页(dirty)。
- 把这次修改顺序写入 redo log(经 Log Buffer)——这是顺序 I/O,极快。
- 事务提交时按
innodb_flush_log_at_trx_commit 把 redo 刷盘(保证持久性)。
- 异步 脏页由后台 page cleaner 线程稍后刷回数据文件(见 s8)。
- 对客户端返回「成功」——此时数据文件里的旧值还在。
图 6 · 写路径:改内存(快)+ 写 redo(顺序,快)→ 立即返回;数据文件落盘是后台异步的(慢,但不阻塞查询)
关键洞察: 直接往磁盘写是随机写(慢);但 InnoDB 把「随机写」拆成了「内存改(快)+ 顺序写 redo(快)+ 异步随机刷脏(慢但不阻塞)」。崩溃时用 redo 重放即可恢复未刷盘的修改——这叫 WAL(Write-Ahead Logging,预写日志)。
「脏」不可怕,关键是控制刷盘时机与节奏,别让脏页堆积或集中爆发。
谁在刷:InnoDB 的后台 page cleaner 线程,按 innodb_io_capacity(默认 200,SSD 可上调)控制每秒刷盘量。
触发刷盘的五种情况:
| 触发条件 | 说明 | 紧急性 |
| 脏页比例超阈值 | 超过 innodb_max_dirty_pages_pct(默认 90%)开始积极刷 | 常态 |
| Free List 不够 | 没空闲帧了,被迫先刷脏页腾位(读请求会卡一下) | 紧急 |
| redo 快写满 | checkpoint 推进,必须把对应脏页刷掉才能覆盖旧 redo | 最紧急 |
| 后台定时 | master 线程约每 1 秒唤醒一次,刷一点 | 常态 |
| 正常关闭 / FLUSH | shutdown 或 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 内部,所以调大 Buffer Pool 也等于给了 Change Buffer 更多空间;
innodb_change_buffer_max_size 还能限制它最多占池子的百分比(默认 25%)。
给多大、几个实例、怎么看命中率——都是运维高频问题。
配置建议
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 / 索引问题导致工作集异常大。
把最容易讲错的点一次性讲清。
误区 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 区。
填两组监控值,看 Buffer Pool 是否健康。
一句话收尾
- 它是什么?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 系列三篇