__v字段仅在save()中生效,updateOne等操作默认绕过校验;需显式用{ _id, __v: old }查询并$inc __v,再检查matchedCount是否为1。

直接用 versionKey 默认字段 + save() 的乐观锁机制,就能拦截大部分并发写入导致的版本覆盖。但前提是字段类型、更新路径和错误处理都得对,否则会静默失败或报错不明确。
为什么 __v 字段没起作用?
Mongoose 默认启用版本控制,会在文档里加一个 __v 字段(整数),每次 save() 成功就自增。但它只在以下情况生效:
-
save()方法调用时才检查,updateOne()、findOneAndUpdate()等批量操作默认绕过__v - 模型定义里显式关闭了
versionKey: false,或手动设为versionKey: 'version'但没同步改校验逻辑 - 你用
doc.set({ ... })赋值后没触发变更检测(比如直接改了doc.__v值) - 从数据库查出文档后,中间有其他写入,但你的代码没捕获
VersionError就直接save()
save() 报 VersionError 怎么处理?
这不是异常,是预期行为。Mongoose 在 save() 发现当前文档 __v 和 DB 里不一致时,会抛出 VersionError。你需要主动 catch 并决定重试还是放弃:
- 捕获
err.name === 'VersionError',而不是靠try/catch捕所有错误 - 重试前建议
await doc.reload()拉最新数据,再合并业务逻辑变更 - 别在重试里无限制循环——加个最大重试次数(比如 3 次),超限就返回冲突状态给前端
- 如果业务允许“最后写入获胜”,那就不用
save(),改用findOneAndUpdate({ _id, __v: oldV }, { $set: ..., $inc: { __v: 1 } }, { runValidators: true })
嵌套数组更新时 __v 会失效吗?
会。只要更新路径不经过 save() 全文档写入,__v 就不参与校验。比如:
-
updateOne({ _id }, { $push: { items: newItem } })—— 不校验__v -
findOneAndUpdate({ _id }, { $set: { 'items.0.name': 'x' } })—— 同样跳过__v - 真正要保版本一致性,就得把整个文档
find()出来,改完再save() - 如果必须用原子更新,就得自己在 query 条件里加
__v: expectedV,失败时由应用层判断是否重试
版本冲突真正的难点不在配置,而在你是否清楚每一次写操作走的是哪条路径:是全量 save、局部 update,还是直接驱动命令。混用这些方式又不统一处理 __v,问题就会藏在看似成功的响应背后。











