SELECT FOR UPDATE必须在显式事务中才生效,单独执行不加锁;需BEGIN后执行FOR UPDATE触发当前读,再业务判断和UPDATE,最后COMMIT;非索引字段可能升级为间隙锁或表锁。

SELECT FOR UPDATE 必须在事务中才生效
单独执行 SELECT FOR UPDATE 不会加锁,MySQL 会直接报错或静默降级为普通查询(取决于隔离级别和 autocommit 状态)。它只在显式事务块内起作用——也就是必须先执行 BEGIN 或 START TRANSACTION,再执行带 FOR UPDATE 的查询,最后用 COMMIT 或 ROLLBACK 结束。
常见错误现象:ERROR 1205 (HY000): Deadlock found when trying to get lock 或查到数据却没锁住、并发更新仍覆盖成功。
- 确保
autocommit = 0,或显式用BEGIN - 不要在存储过程/触发器里隐式依赖自动提交行为
- InnoDB 表必须有主键或唯一索引,否则会升级为表级锁(锁住所有行)
WHERE 条件决定锁的粒度和范围
SELECT FOR UPDATE 加的是「当前读」下的记录锁(Record Lock),但实际锁定范围受 WHERE 条件是否命中索引、是否为等值查询、是否使用范围条件等影响。例如:
SELECT * FROM orders WHERE id = 100 FOR UPDATE;
——只锁 id = 100 这一行(假设 id 是主键);
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;
——若 status 无索引,InnoDB 可能全表扫描并锁住所有行;若有索引但非唯一,会锁住所有匹配的索引记录 + 对应的聚簇索引记录,还可能包含间隙锁(Gap Lock)。
- 务必用
EXPLAIN检查执行计划,确认走了索引 - 避免在
FOR UPDATE查询中使用函数、类型转换或OR条件,容易导致索引失效 - 范围查询(如
WHERE id > 100)会触发间隙锁,可能阻塞插入,需评估业务是否可接受
不同隔离级别下锁行为有差异
在 READ COMMITTED 下,SELECT FOR UPDATE 只加记录锁,不加间隙锁;而在默认的 REPEATABLE READ 下,会同时加记录锁 + 间隙锁,防止幻读。这意味着后者更容易发生锁等待甚至死锁。
- 如果业务允许幻读,且只关心单行强一致性,可临时将事务设为
READ COMMITTED:SET TRANSACTION ISOLATION LEVEL READ COMMITTED; - 但注意:该设置仅对当前事务有效,且不能解决唯一键冲突场景下的插入竞争
- 高并发写入场景下,
REPEATABLE READ的间隙锁可能让两个事务互相等待对方释放间隙,形成死锁
别忽略锁超时和死锁处理逻辑
MySQL 默认 innodb_lock_wait_timeout = 50 秒,超时后抛出 ERROR 1205 或 ERROR 1213。应用层不捕获就崩,更别说重试了。
- 在代码中必须监听
Deadlock found when trying to get lock和Lock wait timeout exceeded这两类错误 - 死锁通常建议立即重试(因 InnoDB 已回滚其中一个事务),超时则需判断是否延长等待或降级处理
- 避免在长事务里持有
FOR UPDATE锁太久——比如锁住后做 HTTP 调用、文件读写等外部操作
真正难的不是写那条 SELECT FOR UPDATE,而是判断哪几行要锁、锁多久、失败后怎么兜底。尤其当业务涉及多表联合校验+更新时,锁顺序不一致就是死锁温床。


















