唯一键冲突会直接消耗自增值,innodb在报错前已预分配并递增auto_increment计数器,导致跳号;事务回滚、批量插入、高并发及手动设值等均加剧该现象。

唯一键冲突会直接消耗自增值
只要插入语句触发了唯一索引(如 UNIQUE KEY)检查,InnoDB 就会在报错前完成自增 ID 的预分配。这个过程不可逆,哪怕最终插入失败,AUTO_INCREMENT 计数器也已递增。
常见错误现象:Duplicate entry 'xxx' for key 'yyy' 报错后,下一条成功插入的记录 ID 跳号(比如从 5 → 7)。
- 不是预编译语句(
PREPARE/EXECUTE)特有,普通INSERT同样发生 - 即使表只有单列主键 + 一个
email唯一索引,用户重复注册也会导致跳号 -
REPLACE INTO和INSERT ... ON DUPLICATE KEY UPDATE行为不同:前者会先删后插,可能分配两个 ID;后者不分配新 ID
事务回滚不释放已分配的自增 ID
InnoDB 在语句解析阶段就向计数器申请下一个值,而非等到 COMMIT 或写盘完成。因此,回滚只丢数据,不丢 ID。
典型场景:调试时频繁 BEGIN + INSERT + ROLLBACK,很快发现 ID 从 100 跳到 105,但表里实际只存了 2 条记录。
- 执行
INSERT INTO t VALUES (NULL, 'a', 'b');后立刻ROLLBACK,AUTO_INCREMENT仍会 +1 - 高并发下多个事务并行申请,计数器可能一次跳多个(尤其
innodb_autoinc_lock_mode = 1或2) - MySQL 8.0+ 持久化了自增值,但回滚行为未改变——持久化解决的是重启丢失问题,不是跳号问题
批量插入和 innodb_autoinc_lock_mode 加剧跳号
当使用 INSERT ... SELECT、LOAD DATA 或大批量 INSERT 时,InnoDB 默认按“预留块”方式分配 ID,而非逐行申请。这是性能优化,但代价是浪费。
例如当前 AUTO_INCREMENT = 100,执行 INSERT INTO t SELECT NULL, c, d FROM src LIMIT 3;,计数器可能直接升至 108 —— 即使只插入 3 行。
-
innodb_autoinc_lock_mode = 0(传统锁模式)可保证连续,但会锁表,严重拖慢并发 -
= 1(默认)对简单 INSERT 连续,对批量操作指数预留(1→2→4→8…) -
= 2(交错模式)完全无锁,但跳号最剧烈,且主从复制需binlog_format = ROW
删除和手动设置也会造成空洞
DELETE 不影响计数器,这是设计使然:InnoDB 只维护“最大用过值 + 1”,不追踪哪些 ID 已释放。
容易被忽略的点:
- 手动
INSERT INTO t (id, name) VALUES (100, 'x');会把计数器设为MAX(100, current) + 1,后续自动插入从 101 开始 - 执行
ALTER TABLE t AUTO_INCREMENT = N;仅重置计数器起点,不填补已有空洞 - MySQL 5.7 及更早版本重启后,
AUTO_INCREMENT会重算为MAX(id) + 1,可能导致倒退(如删掉最大 ID 后重启,计数器变小)
SELECT MAX(id) + 1 插入)反而引入竞态与死锁。重点应放在:是否真依赖连续性?能否用逻辑 ID(如订单号)替代物理主键暴露?











