MySQL自增主键一定是连续的吗?
自增主键”跳号”是 InnoDB 的正常现象,官方语义只保证单调递增,从不承诺连续。弄清缺口产生的机制,才能避免把它误当 bug,更不会在业务上错误依赖连续性。
问题的本质:自增值只进不退
InnoDB 的自增值是一个表级计数器(8.0 起通过 redo log 持久化,重启不回退;更早的版本只存内存,重启后按 MAX(id)+1 重建)。它的分配原则是:插入前申请,申请后只增不减。无论这次插入最终成功、失败还是被回滚,发出去的号都不回收——这是所有”跳号”的总根源。
为什么不回收?反证一下:假设事务 A 拿了 id=1、事务 B 拿了 id=2,A 回滚后若把计数器退回 1,而 B 已提交,后续事务就会再次拿到 2,直接主键冲突。要避免就得把自增锁持有到事务结束,并发性能无法接受。InnoDB 的取舍是:宁可浪费,绝不重号。
四类典型缺口场景
唯一键冲突
INSERT INTO user (id, phone) VALUES (NULL, '13800000000'); |
执行顺序是:先取当前自增值填入记录,推进计数器,再做唯一性检查。冲突时语句失败,但计数器已经加一,下一条插入便跳了一个号。
事务回滚
BEGIN; |
回滚只撤销数据变更,不回退自增计数器,原因如上节所述。
批量插入的预分配
INSERT ... SELECT、LOAD DATA 这类语句事前无法知道要插多少行,InnoDB 按 1、2、4、8…… 翻倍的方式批量预申请 id,语句结束时没用完的号段直接作废。innodb_autoinc_lock_mode 控制分配策略:0 是传统表级锁;1(默认)下简单 INSERT 用轻量互斥锁精确分配,批量语句仍走预申请;2 全部走轻量锁,并发最高,但同一语句的 id 可能与其他语句交错,且在基于语句的 binlog 复制下不安全。注意普通多值 INSERT(一条语句多个 VALUES)能提前算清行数、一次性精确申请,不在此列。
主从切换与重启
8.0 之前自增值只存内存,重启后按 MAX(id)+1 重建——若删掉最大 id 的行再重启,计数器会回退,甚至可能引发主键冲突;8.0 起变更记入 redo log 得以持久化。主从切换后新主重新初始化计数器,加上双写场景常用 auto_increment_offset 与 auto_increment_increment 做奇偶错开,都会带来表面上的”不连续”。
为什么不能依赖连续性
业务若拿自增 id 当订单号、发票号,缺口就是事故:对账对不上、审计说不清,竞争对手还能从 id 差值推算你的单量。正确的分层是:自增 id 只做数据库内部的 surrogate key——聚簇索引有序插入、页分裂少,这是它真正的价值;业务编号交给独立发号方案,如雪花算法、号段模式、UUID v7 或带业务规则的单号服务,唯一性与连续性由发号器自己保证。
小结
自增主键只承诺”单调递增、全局唯一”,不承诺连续。唯一键冲突、事务回滚、批量预分配、重启与主从切换都会制造缺口,而且全是刻意为之的设计取舍。
面试追问:
- 为什么事务回滚时自增值不回退?(避免并发下重号,否则自增锁要持有到事务结束,并发度崩塌)
innodb_autoinc_lock_mode=2有什么风险?(并发最高,但语句级 binlog 下主从数据可能不一致)- 删掉最大 id 的行再重启会怎样?(8.0 之前计数器回退到 MAX+1,8.0 起由 redo log 持久化,不回退)

