SELECT ... FOR UPDATE 在查询过程中即对命中行加排他锁(X锁),需在事务中使用,仅InnoDB支持,是否行锁取决于WHERE条件是否走索引;乐观锁通过version字段实现“读-改-比-写”,失败返回0行影响;选锁策略应基于冲突概率与业务容忍度。

SELECT ... FOR UPDATE 是怎么加锁的
它不是“查完再加锁”,而是查询过程中就对命中行加上排他锁(X锁),其他事务无法对该行 UPDATE、DELETE,甚至无法再用 SELECT ... FOR UPDATE 获取同一行——会阻塞或超时。但注意:SELECT ... LOCK IN SHARE MODE 允许并发读,只是禁止写。
- 必须在事务中使用(
BEGIN/COMMIT包裹),否则锁在语句执行完就释放,失去意义 - 只对 InnoDB 有效;MyISAM 不支持行级锁,
FOR UPDATE会退化成表锁 - 是否加行锁,取决于 WHERE 条件是否走索引:主键/唯一索引 → 行锁;无索引 or 范围扫描过大 → 可能锁住多行甚至全表
- MySQL 没有
FOR UPDATE NOWAIT,想避免无限等待,得靠应用层设innodb_lock_wait_timeout或用GET_LOCK()做轻量预检
乐观锁靠 version 字段怎么防覆盖
它不依赖数据库锁机制,而是在应用层用“读-改-比-写”四步完成更新:先读出数据和 version,构造带条件的 UPDATE,只在 version 未变时才生效。失败时不是阻塞,而是返回影响行数为 0,由业务决定重试或报错。
-
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 123 AND version = 42;—— 这条语句要么成功扣减并升版本,要么静默失败 - 不能只靠
WHERE id = ?,必须带上version条件,否则失去乐观控制意义 - 时间戳(
updated_at)也可替代version,但要注意 MySQL 的NOW()精度(秒级)可能导致并发更新误判,建议用BIGINT自增版号 - 注意:乐观锁不阻止并发读,也不阻止其他字段被修改(比如没参与 version 控制的备注字段),它只保你关心的那几列不被覆盖
什么时候该选悲观锁,什么时候该选乐观锁
不是看“听起来哪个更高级”,而是看业务写冲突概率、响应要求、以及能否接受重试。
- 选悲观锁:库存扣减、订单支付、账户余额变更等「写多读少 + 强一致性 + 不允许脏读/幻读」场景;用户操作链路短(如点击下单→立刻扣库),能容忍短时阻塞
- 选乐观锁:点赞计数、浏览量累加、配置项灰度开关等「读远多于写 + 冲突极少 + 失败可重试」场景;系统已存在长事务或高并发读,不想因锁拖慢整体吞吐
- 混用也常见:比如用乐观锁做最终更新,但关键路径前用
SELECT ... FOR UPDATE做预占(如秒杀预扣库存),再异步落库 - 千万别在日志类、统计类表上滥用悲观锁——这类表本就不需要强一致,加锁反而制造瓶颈
最容易被忽略的三个坑
不是语法写错,而是设计和部署层面的隐形雷。
- 事务没提交或异常没回滚 →
FOR UPDATE锁一直挂着,后续所有相关行操作全卡死;务必检查所有try/catch分支里都有ROLLBACK或COMMIT - 乐观锁重试没加限制 → 网络抖动+高并发下可能无限循环重试,把 DB 打满;建议加最多 3 次重试 + 指数退避
- 没验证隔离级别 →
READ COMMITTED下FOR UPDATE只锁当前读到的行;但REPEATABLE READ下还会加间隙锁(Gap Lock),可能意外锁住不存在的记录,导致插入被阻塞
实际项目里,悲观锁常出现在核心交易链路的 SQL 层,乐观锁更多藏在 ORM 的 update 方法或自定义 DAO 封装里。真正难的从来不是写对一句 FOR UPDATE,而是判断哪条数据值得锁、锁多久、谁来承担锁失败的成本。










