根本原因不是语句本身锁全表,而是repeatable read下select部分触发当前读,对全表扫描路径上的每行加next-key lock(行锁+间隙锁),导致等效锁表现象。

它不会“锁住整张源表”,但可能在 RR 隔离级别 + 无索引扫描时,等效于锁表——因为 next-key lock 覆盖了几乎所有行和间隙。
为什么源表会被大量加锁而不是只锁匹配行
根本原因不是语句本身要锁全表,而是 InnoDB 在 REPEATABLE READ 下对 SELECT 部分执行“当前读”(current read),并对扫描路径上的每一条记录加 next-key lock(行锁 + 间隙锁)。
- 若
WHERE条件没走索引(比如status LIKE '%draft%'),就会全表扫描聚簇索引 → 锁住所有已扫描的行及其间隙 - 即使只查 10 万行,若这些行分散在不同索引页、且间隙跨度大,也会产生数万级
row lock(s)和lock struct(s) - 其他事务想更新/删除这些行,或在间隙中插入新数据,都会被阻塞 —— 表现就是“写不进去”
-
SHOW ENGINE INNODB STATUS中能看到类似44308 row lock(s)的输出,这就是实际锁住的粒度
哪些情况会让锁范围急剧扩大
真正让锁从“几行”变成“几乎全表”的,往往就几个配置或写法问题:
- 隔离级别是
REPEATABLE READ(MySQL 默认),而业务其实能容忍READ COMMITTED:后者只加记录锁,不加间隙锁 - 源表缺主键或唯一索引,或
WHERE条件无法命中现有索引(EXPLAIN显示type=ALL) - 用了
ORDER BY但字段无索引,导致排序过程触发额外扫描和锁 -
innodb_locks_unsafe_for_binlog=ON虽可降级为记录锁,但会破坏 binlog 一致性,MySQL 8.0+ 已弃用,不建议碰
怎么验证当前语句到底锁了什么
别猜,直接看 InnoDB 实时锁状态:
- 在执行
INSERT INTO ... SELECT的同时,新开一个会话运行:SHOW ENGINE INNODB STATUS\G - 重点找
TRANSACTIONS部分下的lock struct(s)和row lock(s)数量变化 - 再查
SELECT * FROM performance_schema.data_locks(MySQL 5.7+),能精确看到每个事务锁住了哪些索引、哪条记录、什么锁类型 - 配合
EXPLAIN FORMAT=tree看 SELECT 是否走了索引、扫描行数预估是否合理
分批处理时最容易忽略的细节
很多人以为加了 WHERE id BETWEEN ? AND ? 就万事大吉,但实际踩坑点很隐蔽:
- 切片字段必须是**单调递增的主键或唯一索引**,否则区间可能重复或遗漏;非主键字段(如
create_time)存在重复值时,BETWEEN会漏数据 - 每批必须显式
COMMIT,不能依赖 autocommit;否则仍是长事务,undo log 持续膨胀,MVCC 清理卡住 - 目标表有自增主键时,
innodb_autoinc_lock_mode=1(默认)会导致每批插入都抢一次表级 auto-inc 锁,反而比单批次更慢 - 不要用
LIMIT分页(如LIMIT 100000, 100000),偏移量越大,前面扫描的行越多,锁也越多
最常被绕开却最关键的一点:锁问题本质是查询效率问题。加索引、缩小扫描范围、控制事务长度,这三件事做扎实了,锁自然就松了。别总想着“怎么绕过锁”,先问问自己那条 SELECT 有没有被优化到极致。











