select for update锁的不仅是where命中的行,还受索引、隔离级别和查询类型影响:主键/唯一索引等值查询只加记录锁;rr下范围或非唯一索引查询加next-key lock(记录锁+间隙锁);无索引则退化为表锁。

SELECT FOR UPDATE到底锁了哪几行?
它只锁 WHERE 条件实际命中(匹配)的记录,但具体范围受索引、隔离级别和查询条件影响,不是“看起来查到几行就锁几行”那么简单。
常见误解是“锁住 SELECT 结果集的所有行”,实际上 InnoDB 会根据执行计划决定加什么锁:
- 等值查询(
id = 1)且id是主键或唯一索引 → 加 记录锁(Record Lock),仅锁该行 - 范围查询(
id BETWEEN 1 AND 5)在REPEATABLE READ下 → 加 Next-Key Lock(记录锁 + 间隙锁),可能锁住(0,1]、(1,5]、(5,6)这些区间,防止幻读 -
WHERE条件没走索引(如name LIKE '%abc%'且name无索引)→ 退化为 表锁,整张表被阻塞
为什么明明只查一行,却锁住了好几行?
这是 REPEATABLE READ(MySQL 默认隔离级别)下 Next-Key Lock 的典型表现。InnoDB 不只是保护“已有数据”,还要防止“新数据插入导致幻读”,所以会把查询范围前后空隙也锁住。
例如:
SELECT * FROM users WHERE age > 25 FOR UPDATE;
即使表里只有 age=26 和 age=30 两行,InnoDB 也可能锁住 (25,26)、(26,30)、(30,+∞) 这些间隙——只要其他事务试图插入 age=27,就会被阻塞。
这不是 bug,是设计行为。想避免?要么改用 READ COMMITTED(不加间隙锁),要么让查询条件能精确命中唯一索引。
如何确认当前语句实际锁了哪些行/间隙?
不能靠肉眼猜,得查 InnoDB 内部状态:
- 运行
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;看当前活跃事务 - 配合
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;和SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;查锁冲突详情 - 执行
SHOW ENGINE INNODB STATUS\G,在输出的LATEST DETECTED DEADLOCK或TRANSACTIONS部分找lock_mode X locks rec but not gap(纯记录锁)或lock_mode X locks gap before rec(间隙锁)这类标记
注意:INNODB_LOCKS 在 MySQL 8.0.29+ 已废弃,改用 performance_schema.data_locks 表。
SKIP LOCKED 和 NOWAIT 怎么改变锁行为?
这两个修饰符不改变“锁什么”,而是改变“怎么应对已存在的锁”:
-
SELECT ... FOR UPDATE SKIP LOCKED:跳过已被其他事务锁定的行,只返回未被锁的行。适合队列消费场景,避免阻塞 -
SELECT ... FOR UPDATE NOWAIT:不等待,直接报错ERROR 1205 (HY000): Deadlock found when trying to get lock或ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately
它们本身不减少锁范围,但能缓解因锁等待引发的级联阻塞——尤其在高并发更新同一张表多个不同行时,SKIP LOCKED 是真正实用的解法。
真正决定锁范围的,永远是索引是否生效、隔离级别是否默认、查询条件是否精确。别迷信“我只查了一行”,先看 EXPLAIN 输出有没有 type: ALL 或 key: NULL —— 那才是表锁预警信号。











