mysql自增主键不连续是innodb为保障高并发性能与事务隔离性所做的确定性设计,非bug或配置错误;事务回滚、唯一键冲突、批量插入等均会预分配id且不可回收,8.0+通过redo log持久化确保重启不丢失自增值。

MySQL的自增主键不连续不是bug,也不是配置错误,而是InnoDB为保障高并发写入性能和事务隔离性所做的确定性设计。只要没手动指定id、没改innodb_autoinc_lock_mode到极端模式,你看到的“跳号”基本都属于正常行为。
事务回滚后AUTO_INCREMENT值不还原
执行BEGIN; INSERT INTO t VALUES (NULL, 1, 1); ROLLBACK;后,SHOW CREATE TABLE t显示的AUTO_INCREMENT值已+1,且无法撤销。
- 自增ID在语句解析阶段就完成预分配,远早于事务是否提交或回滚
- 若允许回退,高并发下需对计数器加重锁或做唯一性校验,性能代价远高于“跳号”
- 这个行为与
innodb_autoinc_lock_mode无关,只要用NULL或省略id字段就会触发 -
ALTER TABLE t AUTO_INCREMENT = N无法倒拨——若N小于当前最大id,MySQL 8.0+仅发warning,5.7及更早版本静默忽略
唯一键冲突也会消耗AUTO_INCREMENT值
建表含UNIQUE KEY c(c),已有(1, 1, 1),再执行INSERT INTO t VALUES (NULL, 1, 2)报Duplicate entry '1' for key 'c',但AUTO_INCREMENT已升至2。
- 流程固定:引擎先取当前自增值 → 改写SQL中的
NULL→ 再校验唯一约束 → 失败后丢弃整行,但ID不回收 -
INSERT IGNORE和ON DUPLICATE KEY UPDATE同样触发预分配,只是不报错 - 反复触发冲突时,
AUTO_INCREMENT会一路涨上去,中间空洞不可逆 - 不能靠
SELECT MAX(id)判断“可用ID”,它忽略所有已分配未提交/未插入的ID段
批量插入(INSERT SELECT / LOAD DATA)默认跳号严重
默认innodb_autoinc_lock_mode = 1下,INSERT INTO t SELECT NULL, c, d FROM src LIMIT 5可能一次性预申请多段ID(如从100直接跳到108),实际只用其中一部分。
- 这是InnoDB为减少锁竞争做的并发优化,不是配置错误
- 设为2(交错模式)可缓解但不消除,且要求
binlog_format = ROW - 设为0(传统全表锁)能保证连续,但吞吐暴跌,线上禁用
- 查当前模式:
SELECT @@innodb_autoinc_lock_mode;动态修改:SET GLOBAL innodb_autoinc_lock_mode = 1(注意主从一致性) - 真正要控制ID节奏,只能放弃批量语句,改用单条
INSERT循环,或由应用层生成主键(如雪花ID)
MySQL 5.7 vs 8.0+ 重启后行为完全不同
MySQL 5.7及更早版本:自增值只存内存,重启后InnoDB扫描表数据,取MAX(id) + 1作为新起点;MySQL 8.0+:自增值写入redo log并落盘到数据字典,启动时重放日志并与MAX(id)比较取较大者加步长。
- 5.7下,曾插入过
id=1000又删掉,重启后可能回到AUTO_INCREMENT = 101 - 8.0+下,
SHOW CREATE TABLE t输出中的AUTO_INCREMENT值即为当前权威值,重启不丢失 -
ALTER TABLE t AUTO_INCREMENT = 500若未严格大于当前MAX(id),8.0+仅警告,5.7静默忽略——但下次插入若撞上已有id,直接报ERROR 1062 - MyISAM表即使在8.0环境下也走老逻辑,因为它依赖文件头而非redo log
最常被忽略的一点是:空洞本身无害,真正危险的是把id当连续序号用——比如分页依赖WHERE id > ?、导出逻辑假设ID连续、前端展示时硬编码“第N条记录”。这些场景一旦遇到跳号,轻则漏数据,重则业务逻辑错乱,且问题往往延后暴露。











