SELECT ... FOR UPDATE 不一定锁单行,因其加锁粒度取决于WHERE条件是否命中索引:命中主键或唯一索引时加Record Lock(单行锁);无索引、索引失效或范围查询时退化为全表扫描并加表级排他锁;在RR隔离级别下默认使用Next-Key Lock,还锁定间隙以防幻读。

必须在事务中使用 SELECT ... FOR UPDATE,且 WHERE 条件必须命中索引,否则锁会升级为表锁,完全失去并发意义。
为什么 FOR UPDATE 不一定锁单行?
排他锁是否真正作用于单行,取决于查询是否走索引。InnoDB 行锁本质是锁索引记录,不是锁“数据行”本身。
- 主键或唯一索引等值查询(如
WHERE id = 100)→ 精确命中一条索引记录 → 只加Record Lock(单行锁) - 普通索引等值查询(如
WHERE user_no = 'U001')→ 若该字段有唯一约束 → 同样只锁单行;若无唯一性 → 可能触发Next-Key Lock(记录 + 间隙),范围更大 - 没索引、索引失效、或范围查询(如
WHERE age > 25)→ InnoDB 无法精确定位 → 退化为全表扫描 → 加表级排他锁(TABLE X LOCK),阻塞所有对该表的读写
事务隔离级别与锁行为的关系
默认的 REPEATABLE READ 隔离级别下,FOR UPDATE 会自动使用 Next-Key Lock,不只是锁住匹配行,还锁住前后间隙,防止幻读。这在多数业务场景下是保护性的,但容易被忽略其副作用。
- 如果你只需要“当前已存在行不被改”,不需要防插入,可考虑降级到
READ COMMITTED:该级别下FOR UPDATE只加Record Lock,不加间隙锁 - 执行前确认当前会话隔离级别:
SELECT @@transaction_isolation; - 临时修改(仅当前事务):
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
常见误用导致死锁或阻塞
死锁往往不是因为用了 FOR UPDATE,而是因为多行加锁顺序不一致或锁范围超出预期。
- 避免在同一个事务里对多行按不同顺序加锁(如事务A先锁
id=5再锁id=3,事务B反向操作)→ 极易死锁 - 不要在
FOR UPDATE后执行耗时操作(如远程调用、复杂计算)→ 锁持有时间越长,阻塞越久 - 检查执行计划:
EXPLAIN SELECT * FROM user WHERE id = 1 FOR UPDATE;→ 确保type是const或eq_ref,而非ALL或index - 注意隐式类型转换:比如
id是INT,但传了字符串'1'→ 可能导致索引失效 → 表锁
验证锁是否生效及排查阻塞
光看 SQL 写得对不对不够,得确认锁真正在起作用,且没被意外释放或升级。
- 查当前持有排他锁的事务:
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'RUNNING' AND trx_mysql_thread_id IN (SELECT blocking_trx_id FROM information_schema.INNODB_LOCK_WAITS); - 查锁等待关系:
SELECT * FROM information_schema.INNODB_LOCK_WAITS; - 查被锁的具体行(需配合
INNODB_LOCKS,MySQL 8.0+ 已移除,改用performance_schema.data_locks) - 最简验证法:开两个终端,Session A 执行
BEGIN; SELECT * FROM user WHERE id = 1 FOR UPDATE;,Session B 立即执行相同语句 → 应卡住,说明锁生效;Ctrl+C 中断 Session B 后再查SHOW PROCESSLIST,能看到状态为Locked
最容易被绕过的点是 autocommit:哪怕写了 FOR UPDATE,如果不在事务里(autocommit=1),锁会在语句执行完立刻释放,根本起不到保护作用。务必以 BEGIN 或 START TRANSACTION 显式开启,且后续跟 COMMIT 或 ROLLBACK —— 中间任何环节出错未处理,锁就一直挂着。


















