根本原因是InnoDB为保证可重复读隔离级别下的语义一致性,必须对源表加shared next-key lock,防止主从不一致或事务内结果不一致;在READ-COMMITTED下可避免源表加锁,因改用一致性读。

MySQL 中 INSERT INTO ... SELECT 会锁表,根本原因不是“它想锁”,而是 InnoDB 为保证可重复读(RR)隔离级别下的语义一致性,**必须对源表加锁**——尤其在默认的 REPEATABLE-READ 隔离级别下。
为什么默认 REPEATABLE-READ 下源表会被加 next-key lock
InnoDB 在 REPEATABLE-READ 下执行 INSERT INTO t1 SELECT ... FROM t2 时,会对 t2 中被扫描到的每一行及其间隙加 shared next-key lock(共享型临键锁)。这不是优化器“多此一举”,而是为了防止:在语句执行过程中,其他事务修改或删除这些行,导致主从不一致(尤其 binlog_format=STATEMENT 时),或破坏当前事务内多次执行该语句结果的一致性。
- 即使
SELECT部分只读,InnoDB 也视其为“当前事务快照的一部分”,不能容忍源数据中途变动 - 如果
SELECT没走索引、触发全表扫描,就会对整张t2表的主键索引加大量 next-key lock,效果等同于“逻辑表锁” -
EXPLAIN中key为NULL或type是ALL,基本可判定会锁全表
READ-COMMITTED 能避免源表加锁吗
能,但有前提:必须显式设置 transaction_isolation = READ-COMMITTED,且 innodb_locks_unsafe_for_binlog 已废弃(MySQL 5.7.20+ 不再支持),所以只需确保隔离级别正确即可。
- 在
READ-COMMITTED下,SELECT部分走一致性读(consistent read),不加任何锁 - 目标表
t1的插入仍会加INSERT intention lock和行级排他锁,但不影响源表t2的并发 DML - 注意:该变更需全局或会话级设置,不能只在单条 SQL 前加
SET SESSION transaction_isolation = 'READ-COMMITTED'后立刻执行——必须在事务 BEGIN 后、SQL 执行前生效
为什么加了索引还是锁表
因为 InnoDB 的行锁是加在索引上的,不是加在记录上。索引失效,锁就升级。
-
WHERE条件用了函数、隐式类型转换、OR连接未全部覆盖索引列,都可能导致key显示为NULL - 联合索引
(a,b,c),只用WHERE b = ?,无法命中索引,照样全表扫描加锁 - 即使
EXPLAIN显示走了索引,也要看rows_examined:如果值接近表总行数,说明实际扫描范围过大,锁范围也大
INSERT INTO SELECT 死锁常见诱因
死锁往往不是单条语句的问题,而是多事务交叉访问资源的节奏问题。
- 事务 A 先更新
t2某行,再执行INSERT INTO t1 SELECT ... FROM t2 WHERE id = X(需读同一行)→ 锁升级冲突 - 多个
INSERT INTO ... SELECT并发执行,且都扫描重叠的索引范围(如都查status = 'pending'且该字段无索引) - 外层包了大事务(比如 Spring 的
@Transactional),让锁持有时间远超语句本身执行时间 -
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE在源表上加锁逻辑更复杂,比纯INSERT INTO SELECT更容易卷入死锁
真正难处理的从来不是“要不要锁”,而是“锁的范围是否可控、持有时间是否可预测”。一旦涉及百万级数据迁移,别只盯着 SQL 写法——先确认隔离级别、索引有效性、binlog 格式,再考虑分批(LIMIT + WHERE id > ?)、临时表中转或应用层流式拉取。否则,一条看似省事的 INSERT INTO SELECT 就可能变成线上事故的起点。

















