Skip to content

Change Buffer 加速二级索引写入 ​

⭐⭐⭐索引设计与失效5.7 & 8.0Change BufferInsert Bufferinnodb_change_buffering写入优化二级索引

INSERT 吞吐只有 3000:5 个二级索引拖垮写入 ​

某订单明细表有 5 个二级索引(按 user_id、按 product_id、按 status 等),业务大量 INSERT 但很少按这些二级索引查询(都是按主键查)。监控显示 INSERT 吞吐受限,磁盘 IO 高,tps 仅有 3000。但磁盘 IOPS 还有富余,似乎不是磁盘瓶颈。

sql
-- 业务表:5 个二级索引的订单明细
INSERT INTO t_order_detail (order_id, user_id, product_id, status, amount, created_at)
VALUES (?, ?, ?, ?, ?, NOW());
-- 单条 INSERT ~0.3ms,tps 3000(瓶颈)

真实场景

"写多读少"的业务(订单流水、日志、监控数据)常见问题:二级索引越多,INSERT 越慢。Change Buffer 是 InnoDB 在内存中缓存非唯一二级索引变更的机制——INSERT/UPDATE/DELETE 不直接写二级索引 page,而是先在内存中"记一笔",后台线程再 merge 到磁盘。能将 INSERT 的随机 IO 转为顺序 IO。

问题分析 ​

bad.sql ​

sql
-- bad.sql: 默认 innodb_change_buffering=all 已开启
-- 但二级索引太多,buffer pool 中相关 page 经常被淘汰,merge 频繁

-- 查看 Change Buffer 配置
SHOW VARIABLES LIKE 'innodb_change_buffering%';
-- innodb_change_buffering: all
-- innodb_change_buffer_max_size: 25 (占 buffer pool 25%)

-- 查看 Change Buffer 状态
SHOW ENGINE INNODB STATUS\G
-- 关注 "INSERT BUFFER AND ADAPTIVE HASH INDEX" 段
-- merge inserts / merged / purge merges 计数

-- 批量插入 10 万行到有 5 个二级索引的表
SET @start_time = NOW(6);
CALL seed_t_order_detail(100000);  -- 造 10 万行
SELECT TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6)) / 1000 AS ms;
-- 默认配置下 ~30s(tps 3333)

为什么慢 ​

每条 INSERT 在 InnoDB 内部需维护:

索引类型是否进 Change Buffer写入路径
主键(聚簇索引)❌ 必须实时写实时写 data page
唯一二级索引❌ 必须实时写实时写(需检查唯一性)
非唯一二级索引✅ 可延迟Change Buffer → 后台 merge

默认配置已开启 Change Buffer,但仍慢的常见原因:

  1. buffer pool 不足:innodb_change_buffer_max_size=25 表示 Change Buffer 最多占 buffer pool 25%。buffer pool 本身小时,Change Buffer 容量也小
  2. 二级索引 page 已被 buffer pool 缓存:当 page 已在内存中时(最常见情况),Change Buffer 无效化,必须直接写 page
  3. merge 跟不上写入:高并发 INSERT 时,merge 线程来不及处理,Change Buffer 满了后同步写入

优化方案 ​

good.sql ​

sql
-- good.sql: 调大 Change Buffer 容量 + 减少不必要的二级索引

-- 1. 调大 Change Buffer 占 buffer pool 的比例(默认 25% → 50%)
SET GLOBAL innodb_change_buffer_max_size = 50;

-- 2. 确认 Change Buffer 模式(默认 all 已覆盖所有场景)
SHOW VARIABLES LIKE 'innodb_change_buffering';
-- all = inserts + deletes + purges(最全)
-- inserts / deletes / purges / none 可单独控制

-- 3. 业务层优化:删除用不到的二级索引
ALTER TABLE t_order_detail DROP INDEX idx_unused_status;  -- 业务不用 status 查询

-- 4. 重新跑批量插入
SET @start_time = NOW(6);
CALL seed_t_order_detail(100000);
SELECT TIMESTAMPDIFF(MICROSECOND, @start_time, NOW(6)) / 1000 AS ms;
-- 优化后 ~18s(tps 5500),约 1.6 倍加速

原理 ​

Change Buffer 是 InnoDB 在 buffer pool 中为非唯一二级索引的变更开辟的特殊区域:

INSERT INTO t_order_detail (user_id=888, ...) 
  ├─ 主键(聚簇索引)→ 直接写 data page(必须实时)
  ├─ 唯一二级索引 (order_id) → 直接写(需唯一性检查)
  └─ 非唯一二级索引 (user_id, product_id) → Change Buffer!
       ├─ 内存中记录 (page_x, delta)
       └─ 后台线程 merge 到磁盘 page

Change Buffer 关键参数:

参数默认推荐说明
innodb_change_bufferingallall包含 inserts/deletes/purges
innodb_change_buffer_max_size2550(写密集)占 buffer pool 百分比
innodb_change_buffering=inserts关闭 deletes/purges不推荐只缓冲 insert 性能略好但功能受限

何时 Change Buffer 无效:

  • 唯一二级索引:必须实时检查唯一性,不能缓冲
  • page 已在 buffer pool 中:直接更新 page(仍要 redo log 刷盘)
  • change buffer 满:退化为同步写入

对比 ​

bad (默认 25%)good (50% + 少索引)
Change Buffer 容量128MB (512MB × 25%)256MB (512MB × 50%)
二级索引数5 个4 个(删除不用索引)
10 万行 INSERT 耗时~30s~18s
tps33335555
磁盘随机 IO高中(merge 平滑)
指标优化前 (bad)优化后 (good)
访问类型N/AN/A
使用索引实时写 5 个索引Change Buffer 50% + 少索引
扫描行数~30s/10万行~18s/10万行
附加信息磁盘 IO 抖动,Change Buffer 频繁刷新后台 merge 平滑,IO 平坦

🚀 INSERT 吞吐提升 1.6 倍,IO 抖动从 80% 降到 30%

避坑指南 ​

注意事项

  1. Change Buffer 仅对非唯一二级索引有效。主键和唯一二级索引仍需实时写入。在表设计时,尽量避免创建唯一二级索引(除非确实需要唯一约束)——这是 Change Buffer 友好的关键。

  2. innodb_change_buffering=inserts 仅缓冲 INSERT,不缓冲 DELETE/UNDO 操作。如果业务有大量 DELETE,写放大仍严重。推荐保持 all。

  3. Change Buffer 在崩溃恢复期间完全应用。MySQL 异常关闭后重启,Change Buffer 内容会先于 redo log 应用,保证不丢失——这由 InnoDB 内部保证,无需应用层介入。

  4. 监控 merge 速率。SHOW ENGINE INNODB STATUS 中 INSERT BUFFER AND ADAPTIVE HASH INDEX 段的 merge inserts/s 是关键指标。如果远低于 inserts/s,说明 Change Buffer 跟不上写入。

  5. MySQL 8.0.13+ 可在线修改 innodb_change_buffer_max_size。5.7 必须重启。在线调整需谨慎,先小范围验证。

5.7 vs 8.0 差异 ​

特性5.78.0
innodb_change_buffering 默认allall
innodb_change_buffer_max_size 默认2525
在线修改 max_size❌ 需重启✅ 8.0.13+ 部分版本可动态
Change Buffer 占用监控SHOW ENGINE INNODB STATUS同 5.7 + INFORMATION_SCHEMA.INNODB_METRICS
与 innodb_buffer_pool_instances 关系分散到各 instance同 5.7

表设计建议

  • 能用普通二级索引就别用唯一索引——除非业务确实需要唯一约束
  • 写多读少的表(订单流水、日志)应精简二级索引,每个索引都让 INSERT 变慢
  • 写完立即按该索引查的场景(如 INSERT 后立刻按 user_id 查)反而应该让 page 进 buffer pool,避免 Change Buffer 失效

本地复现 ​

bash
# 默认在 MySQL 8.0 上运行
./scripts/run-case.sh 20-change-buffer

# 在 MySQL 5.7 上运行(对比)
./scripts/run-case.sh 20-change-buffer --ver 5.7

# 跳过造数据重跑
./scripts/run-case.sh 20-change-buffer --no-seed

MIT Licensed