根本原因是REPEATABLE READ下SELECT部分加shared next-key lock(记录锁+间隙锁),阻塞其他事务的INSERT/UPDATE;降为READ COMMITTED可避免该锁,但需显式设置且binlog_format必须为ROW,否则报错。

为什么INSERT INTO SELECT会拖长锁持有时间
根本原因不在语句本身,而在它默认的执行方式:在REPEATABLE READ隔离级别下,InnoDB会对源表(SELECT部分)加shared next-key lock——也就是记录锁+间隙锁组合。哪怕你只是读,也会锁住数据行和它前后的“空隙”,导致其他事务对同一范围的INSERT/UPDATE被阻塞。尤其当SELECT涉及多表JOIN、无索引字段或全表扫描时,锁范围可能扩大到整个索引段。
把隔离级别降到READ COMMITTED真有用吗
有用,但有条件:
- 必须全局或会话级显式设置:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED,不能只靠配置文件,因为很多ORM或连接池会重置会话级别 - 确认
binlog_format不是STATEMENT——否则MySQL会自动拒绝降级,报错Variable 'transaction_isolation' can't be set to the value of 'READ-COMMITTED' - 效果是让SELECT部分变为一致性读(快照读),不加任何锁;但目标表(INSERT部分)仍会加
exclusive record lock,这点不变 - 副作用:丢失可重复读语义,比如同个事务内两次SELECT可能看到不同结果,需业务侧确认是否可接受
分批插入比单条优化更关键
再好的隔离级别也救不了一次扫100万行的SELECT。真正缓解锁竞争的,是控制单次操作的数据量和持续时间:
- 用
LIMIT+WHERE id > ?分页,比如每次取5000行:INSERT INTO t2 SELECT * FROM t1 WHERE id > 100000 AND id - 每次执行后立刻
COMMIT,不要包在一个大事务里——InnoDB行锁只在事务提交后释放 - 避免用
ORDER BY RAND()或子查询驱动分页,它们容易导致重复扫描或锁升级 - 如果源表有自增主键,优先按主键切分;没有的话,建一个辅助时间戳字段并加索引,比用
OFFSET安全得多
临时表方案适合哪些场景
不是所有情况都适合建临时表,但它能彻底解耦源表锁定与目标表写入:
- 当源表是核心业务表(如
orders),且不能容忍任何读锁影响时,先CREATE TEMPORARY TABLE tmp AS SELECT ...,再INSERT INTO target SELECT * FROM tmp - 注意临时表只在当前会话可见,不能跨连接;若需多线程协作,改用普通
CREATE TABLE tmp_20261013 LIKE target,最后DROP掉 - 临时表本身不走redo log,写入快,但占用buffer pool,大批量时可能触发刷脏压力,建议配合
innodb_buffer_pool_size监控 - 别忘了给临时表加必要索引——如果后续要JOIN或WHERE过滤,没索引的临时表反而更慢
最常被忽略的一点:即使用了READ COMMITTED和分批,如果源表上存在未提交的长事务(比如一个卡住的SELECT FOR UPDATE),它的undo log会阻止MVCC快照向前推进,导致你的INSERT INTO SELECT仍然被阻塞在“读取旧版本”阶段——查INFORMATION_SCHEMA.INNODB_TRX看trx_started时间,比业务预期长得多的事务,得先干掉。


















