MongoDB乐观锁需用@Version字段+原子更新实现,Spring Data自动校验version,失败时ModifiedCount为0需重试;必须每次重试前重新查询最新stock和version,否则导致假锁或超卖。

MongoDB 乐观锁怎么用 version 字段做库存扣减
直接用 @Version 注解 + 原子更新语句,是 MongoDB 实现乐观锁最稳的路。Spring Data MongoDB 会自动在更新时带上 version 条件,只要数据库当前版本和你读出来的不一致,就一条记录都更新不了——这不是 bug,是设计意图。
- 实体必须声明
@Version字段(类型推荐Long),例如:private Long version; - 读取库存时,一定连带拿到
version值,不能只查stock - 扣减时用
findAndModify或updateOne,条件里必须包含{ _id: ..., version: oldVersion } - 检查返回的
UpdateResult.getModifiedCount():等于 1 才算成功;0 表示被并发覆盖,需重试
为什么不能只靠 stock >= num 判断就提交
因为 MongoDB 没有 MySQL 那种原子级的 “WHERE stock >= #{num}” 条件更新能力(即无法在单条 update 里同时校验数值+版本)。只校验 stock >= num 而忽略 version,等于把乐观锁退化成普通更新,超卖风险照旧。
- 错误写法:
updateOne({ _id: id, stock: { $gte: reduce } }, { $inc: { stock: -reduce } })→ 没带 version 校验,不是乐观锁 - 正确写法:
updateOne({ _id: id, version: oldVersion, stock: { $gte: reduce } }, { $inc: { stock: -reduce }, $inc: { version: 1 } }) - 注意:
$inc: { version: 1 }必须显式写,Spring Data 不会自动帮你加
重试逻辑写不好,乐观锁反而变“假锁”
乐观锁失败不是异常退出,而是业务流中的正常分支。没配好重试,一次冲突就直接返回“库存不足”,用户刷十次全失败,实际库存还剩很多。
- 重试次数建议控制在 3–5 次,再失败就真报错或降级
- 每次重试前必须重新
findById,拿到最新stock和version,不能复用旧值 - 别在事务里重试:MongoDB 单文档更新天然原子,不需要事务包装;加了反而增加锁等待和回滚开销
- 高并发下可加随机退避(如 10–50ms),避免所有线程同一时刻重试造成“惊群”
version 字段被意外修改怎么办
如果业务代码或中间件在非库存操作中也更新了该文档(比如改了商品标题、图片),却没同步更新 version,就会导致后续库存扣减永远失败——因为 version 对不上,但 stock 其实够。
- 严格限制文档更新范围:库存相关操作只改
stock和version,其他字段走独立集合或子文档隔离 - 避免用
save()全量覆盖,一律用updateOne+$set/$inc等原子操作 - 上线前用日志埋点监控
UpdateResult.getModifiedCount() == 0的比例,突然升高往往意味着 version 被误触










