SELECT FOR UPDATE是最稳妥的悲观锁方案,它通过在查询时立即加排他锁实现串行化;乐观锁需配合重试逻辑,否则会静默失败。

直接说结论:在高并发更新同一行数据时,SELECT FOR UPDATE 是最稳妥的悲观锁方案;而乐观锁(如 version 字段)适合读多写少、冲突概率低的场景,但必须配合重试逻辑,否则会静默失败。
为什么 SELECT FOR UPDATE 能堵住丢失更新
它不是“先查再改”的两步操作,而是把查询和加锁合并成一步——在事务内执行 SELECT ... FOR UPDATE 时,InnoDB 会立即对匹配的行加上排他锁(X 锁),后续任何其他事务对该行的 SELECT FOR UPDATE 或 UPDATE 都会被阻塞,直到当前事务提交或回滚。
常见错误现象:
- 只写
SELECT不加FOR UPDATE,即使后面跟UPDATE,中间仍有窗口期被并发事务插入 - 在非事务块中执行
SELECT FOR UPDATE,MySQL 自动提交后锁立刻释放,完全无效 - WHERE 条件没走索引,导致锁升级为表锁,性能雪崩
实操建议:
- 必须包裹在
START TRANSACTION和COMMIT之间 - 确保
WHERE中的字段有索引,否则可能锁全表 - 锁范围尽量窄:用主键或唯一索引定位单行,避免
SELECT ... FOR UPDATE扫描多行 - 业务逻辑尽量轻量,减少持锁时间,否则排队延迟明显
UPDATE ... SET x = x + ? 看似原子,其实不防并发
很多人误以为 UPDATE account SET balance = balance + 10 WHERE id = 123 是原子操作,能天然避免丢失更新。但它只是“单条语句原子”,不是“事务级一致性保障”——两个并发事务仍可能同时读到旧值,各自加 10 后写回,结果只加了一次。
这本质是“读-改-写”三步分离,数据库不会自动帮你串行化。
关键区别:
-
UPDATE ... SET x = x + ?:不加锁(除非隔离级别高或命中索引触发隐式锁),并发下大概率丢更新 -
SELECT ... FOR UPDATE+UPDATE:显式加锁,强制串行化关键路径 -
UPDATE ... WHERE version = ?(乐观锁):不加锁,靠版本比对失败来发现冲突
如果你的业务允许“失败重试”,且冲突不频繁,UPDATE ... SET balance = balance + ?, version = version + 1 WHERE id = ? AND version = ? 是更轻量的选择;否则别省这个事。
乐观锁的坑:没重试 = 白加 version
加了 version 字段但没处理 UPDATE 返回影响行数为 0 的情况,等于没锁。系统不会报错,只是静默忽略第二次更新。
典型错误写法:
UPDATE user_wallet SET amount = amount + 100, version = version + 1 WHERE user_id = 1001 AND version = 5;
如果返回 0 行受影响,说明 version 已被别人改过,当前更新被拒绝——但很多代码直接当成功处理了。
正确姿势:
- 检查 SQL 执行后
affected rows是否为 1,不是则重试(最多 3 次) - 重试前要重新查最新
version和业务值,不能复用旧值 - 避免在长事务里嵌套乐观更新,否则重试成本高、用户体验差
- 注意时钟漂移:如果用时间戳做乐观锁,分布式节点间时间不同步会导致误判
真正难的不是选悲观还是乐观,而是判断哪一行数据在哪个业务路径上会被高频并发修改。锁太重拖垮性能,锁太松丢数据——得根据实际流量、冲突率、失败容忍度去压测验证,而不是抄个方案就上线。

















