select ... for update 必须走索引才能行锁,否则升级为表级锁;需确保where条件命中主键或唯一索引、避免隐式转换、控制事务粒度,并考虑隔离级别对间隙锁的影响。

SELECT ... FOR UPDATE 必须走索引,否则锁表
MySQL 的 SELECT ... FOR UPDATE 在高并发下只锁住目标行的前提是:WHERE 条件中的字段有有效索引,且优化器实际使用了它。如果走全表扫描,InnoDB 会升级为表级锁(更准确说是“锁所有扫描过的记录 + 间隙”,但效果接近卡死写入)。
常见错误现象:UPDATE 或其他 SELECT ... FOR UPDATE 被长时间阻塞,SHOW ENGINE INNODB STATUS 显示大量 lock_mode X locks rec but not gap waiting,但对应 SQL 明明只查一行。
- 用
EXPLAIN看执行计划,确认type是const、ref或range,不是ALL或index - 复合索引要注意最左前缀匹配,
WHERE status = ? AND user_id = ?不能用上(user_id, created_at)索引 - 隐式类型转换会让索引失效,比如
user_id是BIGINT,但传了字符串'123',MySQL 可能放弃索引
WHERE 条件必须精确匹配主键或唯一索引
想真正只锁一行,WHERE 中的字段必须能唯一确定一条记录——也就是主键、或带 UNIQUE 约束的列(或组合)。用普通非唯一索引(比如 status),即使走了索引,也会锁住所有满足条件的行+间隙,不是“指定行”。
使用场景:扣减库存、抢红包、订单状态机推进等需要强一致更新单条记录的逻辑。
- 推荐直接用主键:
SELECT * FROM orders WHERE id = 12345 FOR UPDATE - 如果只能用业务字段(如
order_no),务必加UNIQUE约束,并确认索引生效 - 避免用
SELECT ... FOR UPDATE WHERE status = 'pending' LIMIT 1——这会锁住扫描到的第一行,但不确定是哪行,且可能锁多行
事务要短,别在 FOR UPDATE 后做耗时操作
FOR UPDATE 拿到的是当前读的行锁,只要事务没提交,锁就一直持有。如果在 SELECT ... FOR UPDATE 之后调第三方接口、发消息、做复杂计算,等于把锁占着不动,别人全得排队。
性能影响明显:锁等待超时默认是 50 秒(innodb_lock_wait_timeout),但平均响应时间会随并发线性上涨。
- 把锁粒度压缩到最小:查、改、提交三步尽量原子,中间别掺杂 I/O 或 CPU 密集型逻辑
- 如果必须异步处理,先
SELECT ... FOR UPDATE获取数据和版本号,提交事务释放锁,再用该数据触发后续流程 - 注意应用层重试逻辑——重复执行
FOR UPDATE可能导致死锁,建议加TRY_LOCK类语义(MySQL 8.0.1 可用SELECT ... FOR UPDATE NOWAIT)
READ COMMITTED 隔离级别下间隙锁会退化
默认的 REPEATABLE READ 下,SELECT ... FOR UPDATE 不仅锁记录,还锁间隙(防止幻读),这有时比预期锁得更宽;换成 READ COMMITTED 后,间隙锁被禁用,只锁命中的记录本身——对“只锁指定行”更友好,但也意味着可能插入新记录干扰业务逻辑。
兼容性影响:老项目切隔离级别需全链路验证,尤其涉及范围查询或“不存在则插入”的场景。
- 确认业务能否容忍幻读:比如“查无此单 → 创建订单”,在
READ COMMITTED下可能双写 - 变更方式:连接级设
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,别全局改 - 不是所有情况都适合降级,比如资金类系统仍需
REPEATABLE READ保一致性
最容易被忽略的一点:锁行为高度依赖执行计划,而执行计划又受统计信息、参数绑定、字符集甚至 MySQL 小版本影响。线上出问题时,别只盯 SQL 写法,先 EXPLAIN FORMAT=JSON 看它到底扫了哪些行、用了哪个索引、是否触发了隐式转换。











