insert ... select 在 repeatable-read 下对 select 扫描的所有行加临键锁;改用 row 格式+read-committed 隔离级别可降为记录锁,大幅减少锁冲突。

为什么 INSERT ... SELECT 会锁表或锁住大量行
MySQL 在执行 INSERT ... SELECT 时,默认在可重复读(REPEATABLE-READ)隔离级别下会对 SELECT 部分扫描的**所有匹配行加临键锁(next-key lock)**,即使目标表没索引也会锁住间隙。这不仅阻塞并发写入,还可能触发全表扫描级的锁等待。
根本原因不是语句本身,而是事务隔离级别 + binlog 格式组合下的日志安全要求:如果 binlog_format = STATEMENT,MySQL 必须确保从库能精确重放,因此强制对源表加更强的锁来避免幻读风险。
-
binlog_format = STATEMENT时,INSERT ... SELECT默认走基于语句的复制,要求源表读取结果确定、可重现 → 锁更重 -
binlog_format = ROW时,只记录实际变更的行数据,不依赖源表读取过程的一致性 → 可放宽锁范围(但仍需满足隔离级别约束) - 即便设为
ROW,若未开启innodb_locks_unsafe_for_binlog = ON(已弃用)或使用READ-COMMITTED隔离级别,锁也不会自动变轻
配置 binlog_format = ROW 能直接减少锁吗
不能直接减少锁,但它是解除锁加重机制的前提条件。真正起作用的是:在 ROW 模式下,MySQL 允许将事务隔离级别降为 READ-COMMITTED,而该级别下 INSERT ... SELECT **只对实际读取并插入的行加记录锁(record lock),不加间隙锁(gap lock)**,大幅降低锁冲突概率。
关键点:
- 必须同时修改两个配置:
binlog_format = ROW+transaction_isolation = READ-COMMITTED - 仅改
binlog_format不生效 —— MySQL 5.7+ 默认仍用REPEATABLE-READ,锁行为不变 - 修改后需重启或动态设置(
SET GLOBAL transaction_isolation = 'READ-COMMITTED'),且已有连接不受影响 - 确认是否生效:
SELECT @@transaction_isolation, @@binlog_format;
INSERT ... SELECT 还有哪些锁优化手段
即使用了 ROW + READ-COMMITTED,大范围 SELECT 仍可能因扫描过多行导致锁等待或回滚。实用技巧如下:
- 给
SELECT条件字段加索引,避免全表扫描 → 减少锁行数 - 拆成小批量执行:用
LIMIT+ 自增偏移或时间范围分片,例如WHERE created_at BETWEEN ? AND ? - 显式加
FOR UPDATE或LOCK IN SHARE MODE反而更重,除非你明确需要排他控制,否则别加 - 避免在高并发事务中嵌套
INSERT ... SELECT,尤其源表和目标表有外键或触发器时,锁链会延长 - 检查
innodb_autoinc_lock_mode = 2(默认),确保自增主键插入不额外抢锁
容易被忽略的兼容性与副作用
把 binlog_format 改成 ROW 看似简单,但上线前必须验证:
- 从库必须支持相同版本的 MySQL(ROW 日志格式在 5.1+ 完整支持,但早期 5.1.x 有 bug)
- 某些 UDF 或存储函数在 ROW 模式下无法被复制,会报错
Statement is not safe to log in statement format -
INSERT ... SELECT中若含NOW()、UUID()、@user_var等非确定函数,在 STATEMENT 模式下会警告;改 ROW 后虽不报错,但要注意从库时间/值是否符合业务预期 - ROW 日志体积更大,磁盘 I/O 和网络带宽压力上升,特别是大批量插入场景
最常被跳过的一步是:没检查应用层是否依赖 STATEMENT 模式的 binlog 解析(比如用 mysqlbinlog --base64-output=DECODE-ROWS 之外的方式消费日志),一换就断流。











