高并发INSERT卡在AUTO_INC锁上是锁阻塞而非慢查询,因默认mode=1对INSERT SELECT等批量语句全程持表级AUTO_INC锁,导致其他INSERT排队等待ID分配权;设mode=2可规避该锁,但必须满足binlog_format=ROW、业务不依赖ID连续性、避免混合插入等硬性条件。

为什么高并发 INSERT 会卡在 AUTO_INC 锁上
不是慢查询,是锁阻塞。现象是 SHOW PROCESSLIST 里大量线程状态为 Waiting for table level lock,CPU 被 innodb_autoinc_lock 相关函数吃满;慢日志里单条 INSERT 耗时突增,但 EXPLAIN 显示执行计划完全正常。
根本原因是默认 innodb_autoinc_lock_mode = 1 对 INSERT SELECT、REPLACE INTO ... SELECT、LOAD DATA INFILE 这类语句仍全程持表级 AUTO_INC 锁——哪怕只插 100 行,其他所有并发 INSERT 都得排队等它释放“下一个 ID 的分配权”。
- 影响范围不限于同一张表:若多个表共用同一 auto-increment 缓存段(极少见),也可能间接干扰
- 只影响写入路径:读操作完全不受影响,所以监控可能只看到写 QPS 上不去、复制延迟飙升
-
INSERT ... ON DUPLICATE KEY UPDATE不受此锁影响,但它有自己独立的记录锁队列问题
怎么安全地把 innodb_autoinc_lock_mode 设成 2
设成 2 可彻底跳过 AUTO_INC 锁,但不是“开个开关就提速”,而是换锁行为:用 ID 不连续换掉锁等待。必须同步满足三个硬条件,缺一不可:
- 确认
binlog_format = 'ROW':执行SELECT @@binlog_format,如果不是,必须在配置文件中设置并重启 MySQL;STATEMENT模式下设为2会导致从库复制失败 - 确认业务不依赖自增 ID 的“连续性”:比如用 ID 做分页、做外部系统编号映射、或有硬编码假设“ID+1 就是下一条”的逻辑;一旦设为
2,事务回滚、批量插入部分失败都会留下空洞 - 确认没用
INSERT ... ON DUPLICATE KEY UPDATE+ 自增字段做唯一判断:这种组合在mode=2下可能因预分配冲突触发重试,反而增加开销
线上调整必须写进配置文件:/etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf 的 [mysqld] 段落下添加 innodb_autoinc_lock_mode = 2,然后滚动重启(先从库,再主库)。
mode=2 下 ID 空洞是设计行为,不是 bug
设成 2 后,INSERT 刚插完 id=1001,下一条变成 id=1033,中间空洞不可填——这不是错误,是并发安全的必要代价。
- 事务 A 预占了
100~199,事务 B 预占了200~299,A 回滚后,100~199永远空着 - 空洞不影响查询、索引效率,InnoDB 不靠 ID 连续性做任何优化
- 监控空洞程度可用:
SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='db' AND TABLE_NAME='t'对比实际最大 ID
如果业务强依赖“无跳号”(比如对外暴露 ID 当订单号后缀),就别碰 innodb_autoinc_lock_mode,老实用 mode=1 + 拆小批插入。
别混淆自增锁和间隙锁,它们是两套机制
innodb_autoinc_lock_mode 只管“下一个 ID 怎么分”,不管“UPDATE/DELETE 锁哪几行”。很多同学开了 mode=2 后发现 UPDATE 还卡,其实是间隙锁(gap lock)或临键锁(next-key lock)在作祟。
-
UPDATE orders SET paid=1 WHERE status='pending'卡住,大概率是因为status是非唯一索引,InnoDB 在 RR 隔离级别下锁住了整个'pending'范围,包括间隙 - 查锁等待最准的方法是:
SELECT * FROM information_schema.INNODB_TRX r JOIN information_schema.INNODB_LOCK_WAITS w ON r.trx_id = w.requesting_trx_id JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id -
INSERT VALUES (),(),()批量插入在mode=1下是安全的,用 mutex 分配 ID;但 ORM 框架(如 Django、MyBatis)默认可能拆成单行 INSERT,得抓包确认生成的 SQL
真正容易被忽略的是:改完 innodb_autoinc_lock_mode 后不查 binlog_format,或者误以为所有 INSERT 都受益于 mode=2——结果线上还是卡,却以为参数没生效。


















