根本原因是innodb_autoinc_lock_mode=1下insert...select等语句会持语句级auto_inc表锁直至执行完毕,导致其他插入排队;改mode=2可缓解简单插入竞争,但批量/混合插入仍锁表,且需row格式binlog并接受id不连续。

为什么批量 INSERT 会卡在 Waiting for table level lock?
这不是行锁或间隙锁的问题,而是自增主键分配被堵住了。InnoDB 默认用 innodb_autoinc_lock_mode = 1,只要语句里带 SELECT(比如 INSERT INTO t SELECT ...),就会升级成语句级 AUTO_INC 表锁——整条语句执行完才放,其他所有插入全得排队。
常见现象:SHOW PROCESSLIST 里一堆 Waiting for table level lock,INSERT 单条耗时从几毫秒跳到几百毫秒甚至秒级。
-
INSERT INTO t VALUES (NULL, 'a'), (NULL, 'b')→ 拿 ID 快,释放快 -
INSERT INTO t SELECT * FROM s WHERE id > 1000→ 拿 ID 前就锁表,执行多久锁多久 - 哪怕只有一条
INSERT ... SELECT在跑,其他插入全卡住
怎么让自增 ID 分配不串行?
把 innodb_autoinc_lock_mode 改成 2(交错模式),ID 分配改用轻量 mutex,不同事务能并发获取 ID 段。
但注意:它只对 INSERT INTO t VALUES () 这类简单插入生效;INSERT ... SELECT 和部分显式指定自增值的混合写法,在 mode=2 下仍退化为语句级锁。
- 修改方式:
SET GLOBAL innodb_autoinc_lock_mode = 2或写入my.cnf - 必须确认
binlog_format = ROW,否则主从可能中断 - ID 不再严格连续(回滚后预分配段不回收),业务里别硬依赖
id+1或用 ID 推算分页
批量写法本身怎么降低锁冲突?
即使开了 READ COMMITTED 和 innodb_autoinc_lock_mode = 2,写法不当照样锁排队。核心是控制“锁命中点”的集中度和事务粒度。
- 每批控制在 50–200 行:太大易触发锁升级或超
max_allowed_packet;太小则网络/解析开销抵消收益 - 禁用
autocommit,每批显式START TRANSACTION+COMMIT,避免锁长期持有 - 避免高频撞同一条唯一键:比如用手机号做
UNIQUE,并发插入同一号码会全堵在那条记录的 X 锁上 - 唯一索引字段必须提前去重:应用层用
map[string]struct{}或set()过滤重复order_id、device_id
要不要降级隔离级别?
把隔离级别降到 READ COMMITTED 能禁用间隙锁,只锁实际插入的行。多数写多读少场景(如日志表、订单创建后不二次校验)可接受幻读代价。
但别全局改,连接级就够了:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED。
- 配合
INSERT IGNORE或ON DUPLICATE KEY UPDATE,它们原子执行,不引入额外SELECT FOR UPDATE锁链 - 如果业务真需要 RR 级别的可重复读,又没法改写法,那就得靠队列缓冲:Go 用
chan+ goroutine、Python 用queue.Queue定时批量写入,把秒级突发压成毫秒级匀速 - 队列必须设超时丢弃,防止 MySQL 挂了导致 OOM
真正容易被忽略的是:锁竞争不是单点问题,是隔离级别、写法、参数、索引、事务粒度共同作用的结果。调一个参数或换一种 SQL 写法,往往只是解开了链条上的一环。











