3800 字
19 分钟
MySQL Buffer Pool 与 Change Buffer

Buffer Pool 是 InnoDB 的内存核心——通过将磁盘数据页缓存在内存中,将随机磁盘 I/O 转化为内存访问。其两大核心机制:改进版 LRU(冷热分离) 抵御全表扫描造成的缓冲池污染,Change Buffer(写缓冲) 避免修改非唯一索引时的随机读盘。理解这两者的设计原理和边界条件,是进阶 MySQL 性能调优的必经之路。


目录#

  1. Buffer Pool 核心概念
  2. 改进版 LRU 淘汰算法
  3. Change Buffer 写缓冲
  4. 为什么唯一索引不能使用 Change Buffer
  5. 常见面试题
  6. 总结

1. Buffer Pool 核心概念#

1.1 为什么需要 Buffer Pool#

MySQL 的数据最终存储在磁盘上。如果每次 SELECT 都读磁盘、每次 UPDATE 都写磁盘,数据库的并发能力连 100 QPS 都撑不住。

核心矛盾:

对比维度内存访问磁盘 I/O
延迟量级~100ns~100μs (SSD) / ~10ms (HDD)
速度差距基准慢 1,000 ~ 100,000 倍
并发影响几乎无瓶颈磁盘磁头寻道,高并发直接打满

因此 InnoDB 在内存中开辟了一块极大的空间(通常占物理服务器可用内存的 60%~80%),这就是 Buffer Pool。

1.2 数据页(Page)—— I/O 的基本单位#

MySQL 和磁盘交互的基本单位不是”行”,而是数据页(Page,默认 16KB)。Buffer Pool 的本质就是把磁盘上的数据页缓存到内存里。

┌─────────────────────────────────────────────────────────┐
│ 磁盘 (慢) │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ Page1│ │ Page2│ │ Page3│ │ Page4│ │ ... │ │
│ └──┬───┘ └──┬───┘ └──┬───┘ └──────┘ └──────┘ │
│ │ │ │ │
│ │ 从磁盘加载到内存(慢,随机 I/O) │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Buffer Pool (内存,快) │ │
│ │ ┌──────┬──────┬──────┬──────────┐ │ │
│ │ │Page1 │Page2 │Page3 │ ... │ │ │
│ │ └──────┴──────┴──────┴──────────┘ │ │
│ │ 命中 → 直接返回;未命中 → 从磁盘加载 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

类比理解:磁盘是一个巨大的远端仓库,Buffer Pool 是你手边的临时货架。每次查找数据页,先在货架上找(内存命中),找不到再去仓库搬(磁盘 I/O),搬出来顺手放在货架上。修改数据也是直接在货架上改,后台线程异步将”脏页”同步回仓库。

1.3 Buffer Pool 的关键参数#

参数说明建议
innodb_buffer_pool_sizeBuffer Pool 总大小物理内存的 60%~80%(专用服务器)
innodb_buffer_pool_instancesBuffer Pool 实例数并发高时设为 4~8,减少内部锁竞争
innodb_old_blocks_pctOld 区占比(LRU 冷区)默认 37%(3/8),一般不需调整
innodb_old_blocks_time冷区晋升时间窗口默认 1000ms(1秒)

2. 改进版 LRU 淘汰算法#

内存(货架)总有被塞满的一天。塞满之后该把哪些数据页踢出去?这就引出了 Buffer Pool 最核心的算法——改进版 LRU。

2.1 传统 LRU 的致命缺陷#

传统 LRU:新数据放链表头部,很久没用的数据慢慢掉到链表尾部,满了就从尾部淘汰。

但在 MySQL 中会遭遇灾难级的”缓冲池污染”:

场景还原:
假设 Buffer Pool 里全是热点数据(用户高频查询)。
突然,有人执行了一句没走索引的慢 SQL:
SELECT * FROM user_log; -- 一张几千万行的历史日志表
传统 LRU 的结果:
→ 全表扫描将几千万行冷数据全部加载进 Buffer Pool
→ 冷数据挤占 LRU 链表头部
→ 原本的热点数据被"洗出"淘汰 ❌
→ 接下来的几分钟内,所有正常业务请求全部引发磁盘 I/O
→ 数据库瞬间崩溃 💥

这种”一次全表扫描毁掉整个缓冲池”的现象,就是缓冲池污染(Buffer Pool Pollution)。

2.2 冷热数据分离的改进版 LRU#

MySQL 的解决方案:将 LRU 链表分为两截——冷数据区(Old Sublist) 和 热数据区(New Sublist)。

改进版 LRU 链表结构:
Head ←─────────────────────────────────────────────→ Tail
┌──────────────────────────┬──────────────────────────┐
│ New Sublist (热区) │ Old Sublist (冷区) │
│ 默认 5/8 │ 默认 3/8 │
│ │ │
│ [热点页] ↔ [热点页] ↔ ..│.. [冷页] ↔ [冷页] ↔ ... │
│ │ ↑ │
│ │ 新加载的页放这里 │
└──────────────────────────┴──────────────────────────┘
Midpoint (中点)

三级准入规则:

① 初次加载 → 进入冷区头部
从磁盘首次加载的数据页,不放在热区头部,而是放在冷区头部。
这从根本上杜绝了"一次全表扫描就把热点页挤出"的问题。
② 时间窗口考核(默认 1 秒)
如果数据页在进入冷区后的 1 秒内被再次访问:
→ 说明它只是被全表扫描"顺手"带出来的 → 继续留在冷区,迟早被淘汰
③ 晋升热区
只有数据页在冷区待满 1 秒后再次被查询访问:
→ MySQL 判定"这确实是个高频热点数据"
→ 移动到热区头部 ✅
冷区 → 热区的晋升条件:
新页加载
│
▼
┌───────────────┐
│ 放入冷区头部 │
└───────┬───────┘
│
被再次访问?
│
┌─────┴─────┐
▼ ▼
1秒内 1秒后
│ │
▼ ▼
留在冷区 晋升热区
(即将淘汰) (确认为热点)

效果对比:

场景传统 LRU改进版 LRU(冷热分离)
全表扫描热点全被挤出 → 性能崩溃冷数据进冷区 → 1秒内快速淘汰 → 热点不受影响
真实热点正常工作经历 1 秒考核后进入热区,正常工作
间歇性查询可能挤占热点1 秒内未再次访问 → 留在冷区被淘汰

3. Change Buffer 写缓冲#

3.1 写操作面临的问题#

修改数据(INSERT / UPDATE / DELETE)时,如果目标数据页刚好在 Buffer Pool 里,直接修改内存即可——很快。

但如果这个数据页不在内存里呢? 难道要为了修改一行数据,先把 16KB 的数据页从磁盘读进内存再改?这太慢了!

3.2 Change Buffer 的解决思路#

Change Buffer 的核心策略:如果目标数据页不在内存中,且修改的是非唯一普通索引,那就别读磁盘了——直接把修改操作记录在 Change Buffer 中,立刻返回成功。

Change Buffer 的工作流程:
UPDATE 语句 → 修改非唯一索引列
│
├── 数据页在 Buffer Pool 中?
│ │
│ YES ↓ NO ↓
│ 直接修改 写入 Change Buffer
│ 内存操作 ┌──────────────────────┐
│ 很快 ✅ │ 记录: 对 page_xxx │
│ │ 插入/修改/删除 某行 │
│ │ 无需读磁盘! 🚀 │
│ └──────────┬───────────┘
│ │
└────────────────────────────┘
│
下次读取该页时
│
▼
Merge(合并)
将 Change Buffer 中的修改
应用到刚从磁盘读入的页上

3.3 Merge 的触发时机#

触发时机说明
被动 Merge有读请求需要访问该数据页时,先加载到内存,再应用 Change Buffer 中的修改
主动 Merge后台 Master Thread 定期 Merge(每秒/每 10 秒)
强制 Merge系统压力大、Change Buffer 空间不足、或数据库关闭时

3.4 Change Buffer 的生效条件#

Change Buffer 不是万能的——它有严格的限制条件:

Change Buffer 生效条件检查:
是否修改非唯一普通索引?
│
YES ↓ NO ↓
可以使用 不能使用
Change Buffer (唯一索引必须校验唯一性)
│
▼
数据页在内存中?
│
YES ↓ NO ↓
直接修改 写入 Change Buffer ✅

Change Buffer 不生效的场景:

场景原因
唯一索引的修改必须先读盘校验唯一性,Change Buffer 失去意义(详见第 4 节)
数据页已在内存中直接修改即可,无需 Change Buffer
INSERT ... ON DUPLICATE KEY UPDATE本质也是唯一性校验场景

4. 为什么唯一索引不能使用 Change Buffer#

4.1 根因:唯一性校验#

这个问题的终极答案藏在四个字里:唯一性校验。

逻辑推导链条:
① Change Buffer 的设计初心:
避免将冷数据页从磁盘读入内存 → 省去随机磁盘 I/O
② 唯一索引(Unique Index)的强制任务:
修改/插入前,必须 100% 确认表中没有重复值
③ 怎么检查重复值?
必须将目标数据所在的数据页从磁盘读取到内存中,进行比对
④ 结论:
既然为了"唯一性校验",这个数据页已经被强制拉进内存了,
那直接在内存中的页上修改就行了。
Change Buffer 拼命想省掉的那次"磁盘读 I/O",
在唯一性校验这一步已经被强制消耗掉了。
→ Change Buffer 的核心设计初衷完全失效

4.2 常见误区#

误区正解
“因为唯一索引会导致数据页内顺序变化”❌ 无论唯一还是普通索引,B+ 树插入都会导致页内数据重排序,这不是原因
“唯一索引的数据页不能延迟写入”❌ 不是”不能”,而是”没必要”——反正已经读进内存了
“Change Buffer 只对 INSERT 生效”❌ INSERT、UPDATE、DELETE 都可以,只要满足条件

4.3 面试总结话术#

“唯一索引不能使用 Change Buffer 的根本原因在于唯一性约束的校验机制。更新唯一索引前,MySQL 必须先将目标数据页从磁盘读入 Buffer Pool 中,以判断新插入的值是否违反了唯一性。既然数据页为了校验已经被强制加载进内存了,引擎直接在内存中更新数据页即可。这使得 Change Buffer ‘减少随机磁盘读取’的核心设计初衷彻底失效,因此 Change Buffer 仅对非唯一的普通索引生效。”


5. 常见面试题#

Q1:Buffer Pool 为什么使用改进版 LRU,而不是传统 LRU?#

A1:传统 LRU 在遭遇全表扫描时会产生缓冲池污染——扫描到的大量冷数据会挤占 LRU 链表头部,将真正的热点数据全部淘汰出 Buffer Pool,导致后续所有业务请求触发磁盘 I/O、数据库性能崩溃。

改进版 LRU 通过冷热数据分离解决:新加载的页先放入冷区头部(而不是热区头部),只有经历了一个时间窗口(默认 1 秒)后再次被访问的数据,才会晋升到热区。全表扫描的冷数据在 1 秒窗口内大量涌入冷区,但等不到”1 秒后再次访问”的条件,很快就被淘汰——热点数据不受影响。

Q2:Change Buffer 是什么?它的适用场景和限制是什么?#

A2:Change Buffer 是 Buffer Pool 的一个组件,用于优化写操作。当修改的数据页不在内存中时,不读磁盘,而是将修改操作暂存在 Change Buffer 中,等下次该页被读入内存时再合并(Merge)。

  • 适用:非唯一普通索引的 INSERT / UPDATE / DELETE,且目标数据页不在 Buffer Pool 中
  • 不适用:唯一索引(必须先读盘校验唯一性)、数据页已在内存中

Q3:为什么唯一索引不能使用 Change Buffer?#

A3:唯一索引修改前,必须执行唯一性校验——将目标数据页从磁盘读入 Buffer Pool 以检查是否存在重复值。既然数据页已经被强制读进内存了,直接在内存页上修改即可。Change Buffer 设计的核心目的就是省掉这”一次磁盘读 I/O”,但在唯一索引场景下这个 I/O 已经被唯一性校验消耗掉了,Change Buffer 完全失去意义。

详见第 4 节的完整推导。

Q4:Buffer Pool 中的”脏页”是什么?什么时候刷回磁盘?#

A4:脏页(Dirty Page)是在 Buffer Pool 中被修改过、但尚未写入磁盘的数据页。脏页由后台线程通过 Checkpoint 机制异步刷回磁盘,刷盘时机包括:

  • Redo Log 空间不足(write pos 追上 checkpoint)
  • Buffer Pool 空间不足需要淘汰脏页时
  • 数据库空闲时主动刷盘
  • 数据库关闭时全量刷盘

6. 总结#

6.1 Buffer Pool 与 Change Buffer 全景#

┌─────────────────────────────────────────────────────────────────────┐
│ InnoDB Buffer Pool 架构全景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Buffer Pool (内存池) │ │
│ │ │ │
│ │ ┌──────────────────┬─────────────────────────┐ │ │
│ │ │ New Sublist │ Old Sublist (冷区) │ │ │
│ │ │ (热区 5/8) │ (冷区 3/8) │ │ │
│ │ │ │ │ │ │
│ │ │ 热点页 ←→ 热点页│ 冷页 ←→ 冷页 ←→ ... │ │ │
│ │ │ 普通淘汰 │ 新页进冷区头 │ │ │
│ │ │ │ 1s后再次访问→晋升热区 │ │ │
│ │ └──────────────────┴─────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Change Buffer (写缓冲组件) │ │ │
│ │ │ │ │
│ │ │ 修改不在内存中的非唯一索引页 │ │ │
│ │ │ → 暂存修改操作,跳过磁盘读 │ │ │
│ │ │ → 数据页被读入时 Merge 应用修改 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 磁盘 ←→ Buffer Pool 的交互: │
│ • 读未命中 → 从磁盘加载 16KB 数据页 → 放入冷区头部 │
│ • 写命中 → 直接修改内存中的数据页 → 标记为脏页 │
│ • 写未命中 → Change Buffer 暂存修改 → 避免随机读盘 │
│ • 脏页 → Checkpoint 机制异步刷回磁盘 │
│ │
└─────────────────────────────────────────────────────────────────────┘

6.2 关键设计原则#

原则说明
数据页是 I/O 最小单位默认 16KB,Buffer Pool 缓存的是页,不是行
冷热分离抵御污染全表扫描数据进冷区、快速淘汰,热点数据在热区不受影响
1 秒时间窗口冷区数据必须在窗口外再次被访问才能晋升热区——精准区分”真热点”和”扫描顺手”
Change Buffer 跳过随机读修改不在内存中的非唯一索引页时,不读盘,直接记操作日志
唯一索引必须读盘唯一性校验的刚性需求,使 Change Buffer 失去意义
Merge 是被动触发等待下一次该页被读入时才合并——完美利用了”反正要读盘”的时机

6.3 快速排查清单#

排查点看什么工具/命令
Buffer Pool 大小innodb_buffer_pool_sizeSHOW VARIABLES LIKE '%buffer_pool%'
Buffer Pool 命中率Innodb_buffer_pool_read_requests vs Innodb_buffer_pool_readsSHOW GLOBAL STATUS LIKE '%buffer_pool%'
命中率是否健康命中率 > 99% 为正常,< 95% 需关注计算:1 - reads/requests
Change Buffer 使用量Innodb_change_buffering + StatusSHOW VARIABLES LIKE '%change_buffering%'
脏页比例Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_totalSHOW GLOBAL STATUS LIKE '%pages%'
是否有 Change Buffer Merge 积压Innodb_buffer_pool_pages_misc持续增长说明 Merge 跟不上

6.4 一句口诀#

缓冲池大内存扛,冷热分离防污染;一秒钟窗定冷热,写缓冲省读唯普安。


相关链接#


本文基于 MySQL 8.0 / InnoDB 引擎编写,改进版 LRU 与 Change Buffer 的核心机制在所有 5.5+ 版本中通用。

MySQL Buffer Pool 与 Change Buffer
https://shirone.mysqil.com/posts/mysql/mysql-buffer-pool-与-change-buffer/
作者
HikariTish
发布于
2026-05-27
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 141 天

部分内容可能已过时