SELECT FOR UPDATE 必须在显式事务中执行,且 WHERE 条件需命中索引,否则升级为表锁;锁范围受隔离级别影响,RR 级别下含间隙锁,RC 级别仅行锁但有不可重复读风险。

SELECT FOR UPDATE 必须在事务中执行
单独执行 SELECT ... FOR UPDATE 不会加锁,或者锁立刻释放——因为 MySQL 在自动提交(autocommit)模式下,每条语句都是独立事务。锁只在事务生命周期内有效。
- 务必先执行
START TRANSACTION或BEGIN,再执行SELECT ... FOR UPDATE - 确认当前会话 autocommit 关闭:
SELECT @@autocommit;返回 0 才安全;若为 1,需先SET autocommit = 0 - 忘记
COMMIT或ROLLBACK会导致锁长期持有,阻塞其他事务,甚至引发死锁
WHERE 条件必须命中索引,否则升级为表级锁
SELECT ... FOR UPDATE 的行锁能力完全依赖 WHERE 条件是否走索引。没走索引?InnoDB 会退化为锁全表(或锁所有聚簇索引记录),并发性能直接崩塌。
- 用
EXPLAIN SELECT ... FOR UPDATE检查type是否为const/ref/range,且key显示实际使用的索引名 - 主键、唯一索引、普通索引均可触发行锁;全表扫描(
type: ALL)或索引失效(如对字段做函数操作:WHERE YEAR(created_at) = 2024)必然锁表 - 联合索引要注意最左匹配:查询
WHERE status = ?但索引是(user_id, status),该条件无法使用索引
UPDATE 之前必须先 SELECT FOR UPDATE,且避免中间查其他数据
常见错误是“先查 ID,再查详情,最后更新”——两次 SELECT 分离,中间可能被其他事务修改,导致业务逻辑错乱。悲观锁的核心是“查即锁定”,不能拆开。
- 必须用一条
SELECT ... FOR UPDATE查出所有后续更新所需字段,例如:SELECT id, stock, version FROM products WHERE id = 123 FOR UPDATE - 禁止在
SELECT FOR UPDATE和UPDATE之间执行其他非只读 SQL(如 INSERT / DELETE / 另一个 SELECT FOR UPDATE),这会延长锁持有时间,增加冲突概率 - 如果业务需要校验后才更新(比如库存扣减),把校验逻辑尽量放在 UPDATE 的 WHERE 子句里,例如:
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1,避免应用层判断后再 UPDATE
注意隔离级别与间隙锁(Gap Lock)影响
在默认的 REPEATABLE READ 隔离级别下,SELECT ... FOR UPDATE 不仅锁匹配行,还会锁住「间隙」,防止幻读。这容易导致看似不相关的行也被阻塞。
- 例如表中有 id = 1, 5, 10,执行
SELECT * FROM t WHERE id BETWEEN 3 AND 7 FOR UPDATE,会锁住 (1,5) 和 (5,10) 两个间隙,id=4、6 的插入会被阻塞 - 如果确定不需要间隙锁(比如 WHERE 是精确主键查询),可改用
READ COMMITTED级别:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,此时只锁行,不锁间隙 - 但切记:
READ COMMITTED下不可重复读问题会出现,应用层需自行处理(比如用 version 字段乐观锁兜底)
事情说清了就结束。真正难的不是写那行 FOR UPDATE,而是想清楚锁的范围、生命周期、和它周围所有 SQL 的耦合关系。










