直接改 innodb_autoinc_lock_mode = 2 仅缓解 simple insert(如 insert into t values())的锁争用,对 insert select、混合模式及 on duplicate key update 等仍持语句级 auto_inc 锁;根本原因是 mode=2 不适用于批量/混合写法,且需 binlog_format=row、业务不依赖 id 连续性、sql 避免显隐 id 混用。

直接改 innodb_autoinc_lock_mode = 2 能缓解竞争,但只对 INSERT INTO t VALUES () 类型生效;其他写法(如带 SELECT、混合模式、ON DUPLICATE KEY UPDATE)仍会卡在语句级锁上。
为什么你的 INSERT 总在 Waiting for table level lock
这不是慢查询或磁盘瓶颈,是 InnoDB 在分配自增值时被阻塞。现象非常具体:show processlist 里大量线程显示 Waiting for table level lock,QPS 上不去,单条 INSERT 耗时从几毫秒跳到几百毫秒甚至秒级。
根本原因在于默认的 innodb_autoinc_lock_mode = 1:对 INSERT INTO t SELECT、REPLACE INTO t SELECT、LOAD DATA 这类语句,InnoDB 会全程持有表级 AUTO_INC 锁,直到整条语句执行完才释放——哪怕只插 100 行,其他所有并发 INSERT 都得排队。
- 只有
INSERT INTO t VALUES (),(),()这种纯值插入,在mode=1下才用轻量互斥量,很快释放 -
INSERT INTO t SELECT ...或INSERT INTO t VALUES (1,'x'), (NULL,'y')(混合模式)会强制升级为语句级锁 - 事务回滚后,已预分配的 ID 不回收,导致主键空洞(如刚插完 1001,下一条变成 1033)
设了 innodb_autoinc_lock_mode = 2 却还是卡?检查这三件事
这个参数不是“开就灵”,不验证前提条件,线上大概率白调。
- 执行
SELECT @@binlog_format,结果必须是ROW;如果不是,主从复制可能丢数据或错乱 - 确认业务没硬编码 “ID+1 就是下一条” 或用
id BETWEEN 1000 AND 1010拉取数据——mode=2下跳号是常态,会直接漏数据 - 查你实际写的 SQL:只要含
SELECT、显式指定部分 ID、或用了ON DUPLICATE KEY UPDATE,就根本不走无锁路径
SET GLOBAL innodb_autoinc_lock_mode = 2 立即生效但重启失效,必须写进 my.cnf 的 [mysqld] 段才持久。
不改参数,怎么绕过自增锁瓶颈
真正压垮并发的常是写入模式本身。与其死磕锁,不如换写法:
- 把大
INSERT SELECT拆成小批量多值INSERT:每次 50–200 行,用INSERT INTO t VALUES (),(),()替代全量SELECT - 支付类流水等强幂等场景,优先用
INSERT IGNORE+ 唯一索引(如out_trade_no),避免ON DUPLICATE KEY UPDATE引发二级索引锁升级 - 引入内存队列缓冲写请求:Go 用
chan []byte+ goroutine 定时 flush,Python 用queue.Queue配合定时器,每 100ms 或积满 100 条触发一次批量插入 - 务必设置队列超时丢弃机制——防止下游 MySQL 挂了导致 OOM;比如 Go 中用
select { case ch
最容易被忽略的细节
空洞不可怕,可怕的是业务逻辑依赖连续性;mode=2 下的预分配行为是确定性的,但它的副作用(跳号、ID 不可预测、ON DUPLICATE KEY UPDATE 内部重试)往往在压测阶段不暴露,上线后才集中爆发。真正要盯住的不是锁本身,而是你写的每一行 INSERT 是什么类型、有没有隐式触发锁升级、以及下游系统是否悄悄假设了 ID 的行为边界。











