MySQL更新单行却锁多行,是因InnoDB在REPEATABLE READ下默认用Next-Key Lock:非唯一索引等值查询会锁记录及相邻间隙;无索引时全表扫描加锁;隐式类型转换或函数操作导致索引失效也会引发此问题。

MySQL只更新一条记录却锁住多行,根本不是语句写错了,而是InnoDB在当前隔离级别和索引条件下,自动把锁“撑开”了——它锁的从来不只是你看到的那一行。
REPEATABLE READ 下 Next-Key Lock 是默认行为
InnoDB 默认隔离级别是 REPEATABLE READ,它用 Next-Key Lock(记录锁 + 间隙锁)来防止幻读。哪怕你写的是 WHERE id = 100 这种精确等值条件,只要 id 不是唯一索引或主键,InnoDB 就可能升级为范围锁。
- 如果
id是普通二级索引(非唯一),UPDATE t SET x=1 WHERE id = 100会锁住该索引项对应的所有主键行 + 相邻间隙 - 如果表里
id值是 98、100、102,那么执行WHERE id = 100可能实际锁住 (98, 100] 和 (100, 102] 两个临键区间 - 更隐蔽的是:若
id = 100根本不存在,InnoDB 仍会加GAP锁,比如锁住 (98, 102),阻止其他事务插入 99、101
WHERE 条件没走索引,直接退化为全表扫描
这是最常被忽略的“伪单行更新”陷阱:你以为只改一行,其实 MySQL 正在扫全表加锁。
-
EXPLAIN UPDATE t SET status=1 WHERE user_id = '123'中type显示ALL,说明没走索引 - 原因可能是:
user_id是BIGINT类型,但传了字符串'123',触发隐式转换,索引失效 - 也可能是函数包裹:
WHERE DATE(create_time) = '2024-01-01'→ 改成WHERE create_time >= '2024-01-01' AND create_time - 一旦全表扫描,InnoDB 会对所有扫描到的行加意向锁,甚至升级为表级锁(尤其在低版本或特殊配置下)
联合索引使用不当导致锁范围扩大
联合索引不是“用了就安全”,顺序和覆盖决定锁粒度。
- 建了索引
(a, b),但只写WHERE a = 1:InnoDB 会锁定所有满足a = 1的行,并对这些行的b值之间加间隙锁 - 如果再加
ORDER BY b或LIMIT 1,优化器可能放弃索引下推,回表后重新排序,锁住更多临时结果 - 正确做法:确保
WHERE包含联合索引最左前缀,且尽量让条件能精确命中单个索引条目(如WHERE a = 1 AND b = 2)
SELECT FOR UPDATE 比 UPDATE 更容易锁多行
很多人用 SELECT ... FOR UPDATE 做前置校验,却没意识到它比直接 UPDATE 更危险。
-
SELECT * FROM t WHERE status = 0 ORDER BY id LIMIT 1 FOR UPDATE:即使只取 1 行,ORDER BY+LIMIT在 RR 级别下仍可能锁住整个扫描范围内的所有间隙 - 更糟的是:
status = 0如果没索引,就是全表加锁;即使有索引,也可能因数据分布稀疏而锁住大量无效行 - 替代方案:改用带主键等值条件的原子
UPDATE,例如UPDATE t SET status = 1 WHERE id = ? AND status = 0,靠ROW_COUNT()判断是否成功
真正难处理的不是“怎么加锁”,而是“为什么我什么都没做错,它还是锁多了”——间隙锁不可见、隐式转换无声无息、联合索引的最左匹配规则又容易误判。线上出问题时,第一反应不该是调参数,而是立刻 EXPLAIN 那条 SQL,再对照 SHOW ENGINE INNODB STATUS 里的锁详情,看清楚到底哪几行、哪种锁被拿住了。


















