mysql innodb自增主键不保证连续,只保证唯一性和单调递增;事务回滚、唯一键冲突、批量插入预分配及版本重启机制均会导致id空洞,这是设计使然而非bug。

MySQL InnoDB 的自增主键根本就不是为“绝对连续”设计的,它只承诺唯一性和单调递增,不保证无空洞。这是由底层实现机制决定的,不是配置错误或 bug。
自增值在事务回滚时不会回收
插入语句一执行,InnoDB 就立刻向自增计数器申请下一个值,这个动作发生在事务提交前。哪怕整个事务最终 ROLLBACK,ID 也已被消耗。
- 现象:
BEGIN; INSERT INTO t VALUES (NULL, 'x'); ROLLBACK;后,下一次成功插入得到的是 ID+1,不是原来的 ID - 原因:回滚只撤销数据写入和日志,不触碰已推进的自增计数器
- 影响:高失败率的写入场景(如注册接口频繁撞唯一索引)会快速制造大量空洞
唯一键冲突直接触发 ID 预分配
只要 INSERT 语句走到引擎层、开始处理行数据,自增值就已分配完毕——此时甚至还没做唯一性校验。
- 现象:
INSERT INTO t VALUES (NULL, 'duplicate_email');报错Duplicate entry,但表的AUTO_INCREMENT值已 +1 - 关键点:冲突发生在“分配 ID 之后、写入数据之前”,所以无法回退
- 验证方式:
SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA='db' AND TABLE_NAME='t';
批量插入(INSERT SELECT / LOAD DATA)按块预占 ID
受 innodb_autoinc_lock_mode 控制,默认为 1(连续模式)或 2(交错模式),但无论哪种,都采用“指数预留”策略。
- 行为:执行
INSERT INTO t SELECT NULL, a, b FROM src LIMIT 100,InnoDB 可能一次性向计数器申请 128 个 ID - 结果:哪怕只成功插入 95 行,剩余 3 个 ID 也被跳过,计数器直接跳到起始值 +128
- 注意:
innodb_autoinc_lock_mode=2能提升并发,但会让空洞更不可预测,**完全不能填洞**
MySQL 5.7 与 8.0 的重启行为差异放大空洞感知
自增值是否“看起来更连续”,还取决于 MySQL 版本和是否重启过。
- MySQL 5.7 及更早:自增值纯内存维护,重启后执行
SELECT MAX(id) + 1重置计数器 → 删除末尾行再重启,AUTO_INCREMENT反而变小 - MySQL 8.0+:通过 redo log 持久化自增值 → 重启后保持原值,空洞被完整保留,更容易被业务观察到
- 陷阱:开发环境用 5.7,生产用 8.0,同一套迁移脚本在两边表现不同
真正需要警惕的是把“ID 连续”当成业务逻辑前提的设计,比如用 ID 范围分页、依赖 ID 顺序推断插入时间、或对外暴露 ID 作为序号展示——这些场景下空洞不是异常,而是必然发生的现实。与其对抗机制,不如接受它,并在应用层用 created_at 或单独序列号字段替代对 ID 序列的隐含假设。











