MySQL自增ID空洞并非bug,而是InnoDB为兼顾并发性能与一致性,在innodb_autoinc_lock_mode=1(默认)下对mixed-mode插入预分配ID所致;事务回滚、批量失败或重启均会导致已分配ID丢失,无法彻底消除。

MySQL 自增 ID 出现大量空洞,不是 bug,而是 InnoDB 在并发插入时为保证性能和一致性主动“牺牲”连续性的结果。关键变量是 innodb_autoinc_lock_mode,它直接控制自增值分配策略和锁行为。
innodb_autoinc_lock_mode 的三种取值及实际影响
该参数决定 INSERT 类语句如何获取 AUTO_INCREMENT 值,不同模式对空洞、并发性能、主从一致性影响差异极大:
-
0(传统模式):所有 INSERT 都用表级AUTO_INC锁,串行分配 ID。空洞最少,但写入吞吐极低,已弃用 -
1(默认模式,interleaved):简单 INSERT(如INSERT INTO t(a) VALUES(1))不锁表,预分配一批 ID;批量/混合模式(如INSERT ... ON DUPLICATE KEY UPDATE、INSERT ... SELECT)仍用AUTO_INC锁。这是空洞最常见来源——bulkCreate、replace into 等操作会一次性预占几十甚至上百 ID,失败或回滚后全丢弃 -
2(无锁模式):所有 INSERT 都不加AUTO_INC锁,完全依赖轻量级 mutex 分配 ID。并发最高,但主从复制若用 STATEMENT 格式可能出错(因为 binlog 中 ID 顺序与从库执行顺序不一致),必须搭配 ROW 格式
为什么 bulkCreate 或 ON DUPLICATE KEY UPDATE 特别容易制造空洞
这类语句属于 “mixed-mode inserts”,InnoDB 必须在语句开始前就预估并预留足够多的自增值——即使最终只成功插入几行,预留的 ID 全部作废。例如:
INSERT INTO audit_image_log (id, log) VALUES (NULL, 'a'), (NULL, 'b') ON DUPLICATE KEY UPDATE log = VALUES(log);
这条语句在 innodb_autoinc_lock_mode=1 下,InnoDB 可能预分配 10 个 ID,但只用了 2 个,剩下 8 个永久丢失。
- Sequelize 的
bulkCreate({ignoreDuplicates: true})底层就是这种语法 - 即使没发生冲突,只要语句被归类为 mixed-mode,预分配行为就存在
- 事务回滚时,已分配但未提交的 ID 不会回收
调整 innodb_autoinc_lock_mode 的实操建议
不要盲目设为 2,先确认你的复制格式和业务容忍度:
- 主从复制必须是
binlog_format = ROW,否则设为2会导致从库数据不一致 - 检查当前值:
SELECT @@innodb_autoinc_lock_mode; - 动态修改(无需重启):
SET GLOBAL innodb_autoinc_lock_mode = 2;,但注意该变量不可动态降级(2 → 1需重启) - 真正想减少空洞,优先改应用逻辑:用
INSERT IGNORE替代ON DUPLICATE KEY UPDATE(前者是 simple insert,不触发预分配);避免显式指定部分 ID 值(如VALUES(1,'a'),(NULL,'b'))
空洞无法彻底消除,但可规避最严重的浪费场景
自增 ID 的设计目标从来不是连续,而是唯一、快速、可扩展。哪怕把 innodb_autoinc_lock_mode 调到 2,只要存在事务回滚、批量失败、服务器异常重启(MySQL 5.7 及更早),空洞依然会出现。真正需要连续 ID 的场景(比如对外展示订单号),应该用单独的业务序列号生成器,而不是依赖 MySQL 的 AUTO_INCREMENT。











