自增id空洞的根本原因是事务回滚后已分配id不可回收,而非间隙锁;innodb在insert执行时即预分配并消耗id,无论提交与否,导致如rollback或on duplicate key update冲突后id永久丢失。

innodb_autoinc_lock_mode 配置不当会放大锁竞争,但真正导致“间隙”的根本原因不是间隙锁(Gap Lock),而是事务回滚和自增计数器的不可回收特性。间隙锁本身不生成主键值,它只阻塞插入——所以它不会造成ID空洞,只会让插入卡住。
自增主键出现空洞的真实原因:事务回滚后ID不回收
InnoDB 的自增计数器在事务执行 INSERT 语句时就立即递增并分配 ID,不管事务最终是否提交。一旦分配,这个 ID 就被“消耗”了。
- 事务 A 插入一条记录,拿到
id = 100,随后因唯一键冲突或业务逻辑主动ROLLBACK→id = 100永久丢失 - 事务 B 插入成功,拿到
id = 101,表中实际只有 101,100 成为空洞 - 这种空洞是原子性(Atomicity)的必然代价:不能为了“连续”而牺牲事务回滚的确定性和并发性能
INSERT ... ON DUPLICATE KEY UPDATE 是空洞高发场景
该语句本质是“先尝试插入,冲突则转为更新”。但注意:只要执行了 INSERT 部分,自增 ID 就已申请并消耗,即使最终走的是 UPDATE 分支。
- 假设
machine_id是唯一索引,反复用相同machine_id执行该语句 - 每次都会触发自增计数器 +1,但只有第一次真正插入新行,后续全是更新 → 连续产生空洞
- 错误认知:“没插入新行,就不该占 ID” → 实际上 MySQL 无法预判是否冲突,必须先预留
innodb_autoinc_lock_mode 影响的是并发性能,不是空洞有无
这个参数控制自增 ID 分配时的锁策略,但它不改变“已分配即不可退”的核心行为。
-
innodb_autoinc_lock_mode = 0:表级锁,空洞依然存在,只是并发低、现象不密集 -
innodb_autoinc_lock_mode = 1(默认):轻量 mutex,单条插入几乎无等待,但空洞频率反而更高(因并发高、回滚多) -
innodb_autoinc_lock_mode = 2:无锁分配,空洞最多(尤其配合INSERT SELECT),且主从可能错位(需sync_binlog=1补救)
为什么你看到的“阻塞”常被误认为是间隙锁问题?
当某条 SELECT ... FOR UPDATE 或范围 UPDATE 持有间隙锁时,后续 INSERT 可能被阻塞在“等待插入位置”,但这和自增 ID 空洞无关——它只是暂时卡住,一旦锁释放,插入仍会拿到下一个可用 ID(比如 105),不会跳过 102–104。
- 真正混淆点:日志里看到
lock_mode X locks gap before rec,就以为“间隙锁导致 ID 不连续” - 事实是:间隙锁管的是“能不能插”,自增 ID 管的是“插的时候给什么号”,二者作用域不同
- 如果你观察到插入延迟 + ID 跳变,要分开排查:跳变查事务回滚和
ON DUPLICATE KEY使用;延迟查锁等待和SHOW ENGINE INNODB STATUS中的LOCK WAIT
ON DUPLICATE KEY UPDATE 替代幂等设计,会让空洞密度远超预期。











