在日常使用 MySQL 的 InnoDB 存储引擎时,我们经常会面临一个选择:到底该给字段建普通索引 ,还是唯一索引 ?很多人觉得唯一索引"看起来更严格、更安全",但你可能不知道,在更新频繁的场景下,选择普通索引可能带来更明显的性能提升。背后的原因,就和 Change Buffer 这一机制密切相关。
一、唯一索引为什么不能用 Change Buffer?
对于唯一索引来说,所有的更新操作都必须先判断这次操作是否违反唯一性约束。比如,执行 INSERT INTO t VALUES (4,400),假设 k 是唯一索引列,那么 InnoDB 需要判断表中是否已经存在 k=4 的记录。要完成这个判断,数据页必须被读入内存才能进行比对。一旦数据页已经在内存里了,直接修改内存中的页显然是更直接的做法,根本用不到 Change Buffer 的"暂存修改、延迟合并"的能力。
因此,唯一索引的更新无法使用 Change Buffer,这一特性仅对普通索引生效。
二、Change Buffer 是什么?它解决了什么问题?
Change Buffer 可以理解成一个"写操作的缓存区",它属于 Buffer Pool 的一部分。当需要更新一个普通索引的数据页,而该数据页又不在内存中时,InnoDB 并不会立刻把磁盘上的数据页读进来,而是先将这次更新操作记录到 Change Buffer 中。等到未来有查询需要访问这个数据页,或者由后台线程定期将操作"合并"到磁盘数据页上时,才会完成真正意义上的数据修改。
这样做最大的好处是:减少了随机磁盘读 I/O。数据库中最昂贵的操作就是随机读盘,Change Buffer 让更新操作能够尽量避开随机读盘,从而在高并发写入的场景下显著提升性能。
Change Buffer 使用的是 Buffer Pool 的内存,所以大小不是无限的。可以通过参数 innodb_change_buffer_max_size 动态调整,例如设置为 50,表示 Change Buffer 最多只能占用 Buffer Pool 总大小的 50%。
三、更新操作的目标页在内存 vs 不在内存
我们用插入 (4,400) 的例子,来看普通索引和唯一索引在不同情况下的表现差异。
1. 目标数据页已经在内存中
-
唯一索引:找到 3 和 5 之间的位置,判断无冲突,直接插入值,语句结束。
-
普通索引:同样找到位置,直接插入值,语句结束。
此时两者的差别仅是一个唯一性判断 ,消耗的 CPU 时间微乎其微,不是性能关注的重点。
2. 目标数据页不在内存中(这才是关键)
-
唯一索引:因为必须要做唯一性校验,所以必须将数据页从磁盘读入内存,判断没有冲突后插入值,语句结束。这个过程涉及一次随机磁盘读,代价较高。
-
普通索引:不会将数据页读入内存。而是将这次更新操作记录到 Change Buffer 中,同时写入 redo log 确保持久性,语句就结束了。后续数据页被访问或后台合并时,才会把 Change Buffer 中的操作应用到数据页上。
很明显,避免随机读是普通索引更新比唯一索引快的根本原因,Change Buffer 在其中发挥了巨大的作用。
四、标准流程还原:Change Buffer + Redo Log 的配合
这里需要纠正一个容易出现的理解偏差:并不是"先写 redo log 再改内存",而是修改内存的同时,将操作记录到 redo log 中。WAL(Write-Ahead Log)的核心是保证日志比脏页先刷盘,以便崩溃恢复,并不是指日志要早于内存修改执行。
我们将完整的流程拆解为两种场景(以普通索引更新为例):
场景一:数据页已在 Buffer Pool 内存中
-
直接修改内存中的数据页,该页变为脏页。
-
将修改操作同步写入 redo log(事务提交时刷盘),保证崩溃后也能恢复。
-
事务提交,操作完成。后续由后台线程负责将脏页刷回磁盘。
场景二:数据页不在 Buffer Pool 内存中
-
不加载数据页,不直接修改磁盘(避免了随机读 I/O)。
-
将本次更新操作记录到 Change Buffer 中,同时写入 redo log,确保 Change Buffer 自身的记录不丢失。
-
事务提交,操作完成。此时磁盘上的数据页还是旧数据,Change Buffer 中记录了"待应用的操作"。
-
Change Buffer 中的操作最终会通过 Merge 应用到数据页上,常见的触发场景包括:
数据页被加载到 Buffer Pool 时
当有查询(甚至包括更新操作本身需要读取该页)将该数据页从磁盘读入内存时,InnoDB 会顺便把 Change Buffer 中与该页相关的所有操作应用到内存页上,然后返回最新数据。
后台 Master Thread 定期 Merge
即使该数据页一直没有被用户查询访问,后台线程也会定期主动将 Change Buffer 中的操作合并到数据页上(数据页不在内存时会先加载进来)。
Change Buffer 空间不足时
当 Change Buffer 使用量接近 innodb_change_buffer_max_size 设置的上限,需要写入新的操作记录时,会强制对部分操作进行 Merge,以释放空间。
数据库正常关闭(shutdown)时
在 InnoDB 完全关闭前,会将 Change Buffer 中的所有暂存操作全部 Merge 到对应数据页,保证持久化。
所以,无论数据页后续是否被主动"读",Change Buffer 里的修改都绝不会丢失:要么在读时实时合并,要么由后台或关闭流程帮你合并。
- 合并完成后,产生的脏页与普通更新产生的脏页毫无区别,后续由 InnoDB 统一管理刷盘。
简单理解就是:redo log 让你"写得快、不丢数据",change buffer 帮你"少读盘、延迟修改",二者配合,大幅提升了普通索引的更新性能。
五、如何选择普通索引和唯一索引?
既然唯一索引和普通索引在查询能力上没有差别 ,那么如何选择主要取决于更新性能的考量。
- 优先选择普通索引
在高并发写入、更新频繁的业务场景(例如订单流水、日志表等)下,普通索引能够充分利用 Change Buffer,避免昂贵的随机读,性能提升明显。
- 当更新后紧跟着查询时,考虑关闭 Change Buffer
如果业务模式是每次更新后立刻就要查询这条记录(例如先修改用户积分,然后马上展示),Change Buffer 反而会多此一举:因为查询时会立即触发 Merge,此时原本期望避免的随机读还是发生了,甚至多了维护 Change Buffer 的开销。这种场景可以将 innodb_change_buffering 设置为 none 来关闭 Change Buffer。
- 机械硬盘环境要格外关注
如果使用机械硬盘,随机读的代价极高,Change Buffer 的收益会更加显著。对于成本敏感的"历史数据"库,建议尽量使用普通索引,并将 innodb_change_buffer_max_size 调大(如 50),以确保写入速度。
六、总结
-
唯一索引不能使用 Change Buffer,只有普通索引可以。
-
Change Buffer 的核心价值在于减少随机磁盘读 I/O,将更新操作暂存,延迟应用到数据页。
-
正确的更新流程是:修改内存时同步写 redo log,如果页不在内存则先将操作写入 Change Buffer 并写 redo log;后续访问或后台 Merge 时再加载数据页应用操作。
-
在查询能力相同的前提下,普通索引一般优于唯一索引,尤其适合写多读少、或写入后有较长间隔才读取的场景。
理解这些底层机制后,你在设计表结构和索引时,就能做出更合理的取舍,让数据库的写入性能更上一层楼。