SELECT ... FOR UPDATE 在 WHERE 条件无有效索引时会锁整张表,因全表扫描导致聚簇索引全锁;需用 EXPLAIN 检查执行计划、统一加锁顺序、慎用间隙锁,并优先使用原子操作或唯一约束替代。

WHERE 条件没走索引,SELECT ... FOR UPDATE 会直接锁整张表
这不是“可能”,而是 InnoDB 的确定行为:当 SELECT ... FOR UPDATE 的 WHERE 条件无法命中有效索引(比如字段无索引、用了函数如 DATE(create_time)、或发生隐式类型转换),MySQL 就会退化为全表扫描,并对所有扫描过的行加锁——在可重复读(RR)隔离级别下,这往往等价于锁住整个聚簇索引,也就是事实上的表级锁定。
常见错误现象:
• 并发稍高时大量事务卡在 Waiting for table metadata lock 或 Waiting for global read lock
• EXPLAIN FORMAT=JSON 显示 key_locks 字段为空或范围极大
• SHOW ENGINE INNODB STATUS 中看到 lock_mode X locks rec but not gap 变成 lock_mode X locks table
- 实操建议:每次上线前用
EXPLAIN FORMAT=JSON检查关键FOR UPDATE语句,确认key和key_locks字段是否符合预期 - 复合查询务必建联合索引,例如
user_id = ? AND status = ?就不能只依赖user_id单列索引 - 禁止在事务中执行
SELECT ... FOR UPDATE WHERE name LIKE '%abc'—— 这种前导通配符必然走不了索引
多行更新顺序不一致,是死锁最典型的触发路径
死锁不是“运气差”,而是两个事务以相反顺序访问同一组主键。比如事务 A 先 SELECT ... FOR UPDATE WHERE id IN (5, 2)(实际按索引顺序锁了 2→5),事务 B 同时执行 SELECT ... FOR UPDATE WHERE id IN (2, 5) 却因查询优化器路径不同锁了 5→2,就形成 A 等 B 释放 5、B 等 A 释放 2 的循环等待。
使用场景:
• 批量状态更新(如“把一批订单设为已发货”)
• 分页任务队列消费(SELECT ... FOR UPDATE LIMIT 10)
- 实操建议:强制统一加锁顺序,例如始终加
ORDER BY id ASC,哪怕业务逻辑本身不需要排序 - 避免在同一个事务里混合操作多张表且顺序随意,比如订单服务先锁
orders再锁inventory,支付回调却反过来——这是环形等待的温床 - 用
SELECT ... FOR UPDATE SKIP LOCKED替代LIMIT分页取数,它能跳过已被锁的行,天然规避部分顺序冲突
间隙锁(Gap Lock)在 RR 隔离级别下悄悄扩大锁范围
在默认的可重复读(RR)下,SELECT ... FOR UPDATE 不仅锁匹配的行,还会锁住“值之间的空隙”。例如 WHERE age BETWEEN 20 AND 30,即使表中没有 age=25 的记录,InnoDB 也会锁住 (20,30) 这个区间,阻止其他事务插入 age=25 的新行——这就是间隙锁,它本意是防幻读,但副作用是大幅提高锁冲突概率。
参数差异:
• READ COMMITTED 隔离级别下,间隙锁被禁用,只锁实际存在的行
• SERIALIZABLE 下则更激进,会将范围查询升级为临键锁(Next-Key Lock),覆盖行+间隙
- 实操建议:若业务能接受 RC 级别(比如日志类、统计类场景),可考虑降级隔离级别来关闭间隙锁
- 高频更新的非唯一字段(如
status),不要单独建索引——它容易引发大面积间隙锁;改用联合索引前置该字段,或改用乐观锁 - 避免在事务中先
SELECT ... FOR UPDATE再INSERT同一索引范围,比如先查WHERE status = 0,再插一条status = 0的新记录,极易触发间隙锁冲突
别把 SELECT ... FOR UPDATE 当“安全保险丝”,它本身就是锁冲突源头
很多团队误以为“先查再更”比直接 UPDATE 更可控,结果反而更危险:一次 SELECT ... FOR UPDATE 锁的范围,常常比后续 UPDATE 语句本身需要的还大。尤其是当 SELECT 走的是非唯一索引,而 UPDATE 用的是主键时,前者锁了一片,后者只改一行,白白扩大了竞争面。
性能影响:
• 锁持有时间 = 从 SELECT ... FOR UPDATE 执行完到事务 COMMIT 的整个时长
• 一个 200ms 的计算逻辑放在事务内,就等于让锁多持有了 200ms
- 实操建议:能用原子性
INSERT ON DUPLICATE KEY UPDATE或UPDATE ... WHERE直接完成的,就别拆成两步 - 事务内只保留真正需要锁的最小操作集,把日志记录、HTTP 调用、缓存更新等非数据库操作移出事务
- 对幂等写入场景(如防重下单),优先用唯一约束(
UNIQUE(user_id, order_date))替代SELECT ... FOR UPDATE,失败走应用层重试,既免锁又快
最容易被忽略的一点:锁不是加在 SQL 上,而是加在索引结构上;你写的 SQL 看似简单,但执行计划稍有偏差,锁的粒度就可能从“一行”变成“一万个间隙”。所以别信直觉,每个 FOR UPDATE 都得拿 EXPLAIN 和 INNODB STATUS 对着看。










