select for update只在事务中生效,因其本质是为后续更新加行级排他锁,而autocommit=1时锁执行完即释放;必须显式begin/start transaction并确保@@autocommit=0,否则无法实现串行化保护。

SELECT FOR UPDATE 为什么只在事务里生效
它本质是给查询到的行加写锁,但 MySQL 不允许在自动提交模式下加这种锁——否则一执行完就释放,根本起不到“锁住等你更新”的作用。所以必须显式开启事务:BEGIN 或 START TRANSACTION,且不能让 autocommit=1 干扰。
常见错误现象:单独执行 SELECT ... FOR UPDATE 没报错,但另一会话立刻能改同一行——八成是没进事务,或者事务已自动提交(比如客户端默认 autocommit=1 且没关)。
- 确认当前会话事务状态:
SELECT @@autocommit;,值为0才安全 - 手动关闭自动提交:
SET autocommit = 0;(不推荐长期用,仅调试时明确控制) - 更稳妥做法:始终用
BEGIN; SELECT ... FOR UPDATE; UPDATE ...; COMMIT;包裹完整逻辑
哪些索引能让 SELECT FOR UPDATE 锁住单行而非全表
锁粒度取决于 WHERE 条件是否命中**唯一索引或主键**。如果查的是 id = 100(id 是主键),MySQL 只锁这一行;但如果查的是 status = 'pending' 且 status 没索引,InnoDB 可能升级为表级锁(或锁所有聚簇索引记录),并发性能直接崩。
- 必须确保 WHERE 中的字段有**有效索引**(最好是唯一索引或主键)
- 避免隐式类型转换:比如
WHERE user_id = '123'(user_id是 INT),会导致索引失效,进而锁范围扩大 - 用
EXPLAIN看执行计划:type字段要是const或eq_ref,才大概率是单行锁
UPDATE 和 SELECT FOR UPDATE 的顺序不能颠倒
很多人想“先 UPDATE 再查结果”,但这样无法实现悲观锁意图——UPDATE 本身会加锁,但若没提前锁定,两个事务可能同时读到旧值、同时 UPDATE,造成覆盖写(即丢失更新)。正确顺序是:查(带锁)→ 业务判断 → 改(基于刚查到的值)→ 提交。
示例场景:扣库存,库存为 5,两个请求同时来:
-- 事务A BEGIN; SELECT stock FROM products WHERE id = 123 FOR UPDATE; -- 查到 stock = 5,判断够扣,执行: UPDATE products SET stock = 4 WHERE id = 123; COMMIT;
此时事务B执行同样 SELECT ... FOR UPDATE 会被阻塞,直到 A 提交,B 才能继续——真正串行化。
- 禁止把
SELECT FOR UPDATE和UPDATE拆到不同事务里 - 禁止在 SELECT 后做耗时操作(如调外部 API、sleep),否则锁持有时间过长,拖垮并发
- 如果只是校验后可能不更新,也要锁住——否则校验通过后别人抢先改了,你的 UPDATE 就不安全了
死锁不是小概率事件,而是设计缺陷的信号
当两个事务按不同顺序访问多行时,比如事务A先锁 id=100 再锁 id=200,事务B反过来,就极易触发死锁。MySQL 会主动回滚其中一个(报错 Deadlock found when trying to get lock),但应用层必须捕获并重试。
- 所有涉及多行
SELECT FOR UPDATE的场景,务必按**相同顺序**访问(例如永远按id ASC排序后再锁) - 避免在事务中跨表锁多行,尤其表之间无固定访问次序时
- 监控死锁日志:
SHOW ENGINE INNODB STATUS\G里的LATEST DETECTED DEADLOCK段落,看谁在争哪几行
锁不是万能的,它把并发问题转成了等待和死锁问题。真正难的不是怎么加锁,而是确定该锁什么、锁多久、谁来负责重试——这些都得在业务逻辑里写清楚,不能只靠一句 FOR UPDATE。











