updateone 加 $eq 版本号检查不是乐观锁,因未保证读出的 version 是最新值;真正可行的是用 { _id, version: 旧值 } 查询并 $inc version,依赖 mongodb 单文档写原子性实现校验与升版。

为什么 updateOne 加 $eq 版本号检查不等于乐观锁
直接在 updateOne 里写 { version: { $eq: 1 } } 看似能防止覆盖,但漏掉了关键点:它没保证「读出的 version 值就是当前最新值」。如果两次读之间有其他写入把 version 升到 2,你仍会用旧值 1 去比,结果是“误成功”——这不是乐观锁,是条件竞态。
真正可行的做法必须把「读」和「更新」串成原子操作,靠 MongoDB 的写操作天然原子性来兜底:
- 先
findOne拿出文档和当前version - 业务逻辑处理完后,用
updateOne同时做两件事:检查version未变 + 自增version - 检查
result.matchedCount === 1才算更新成功,否则重试或报错
如何用 $set 和 $inc 一次完成版本校验与自增
MongoDB 不支持在 update 中引用原字段值做条件(比如 { version: oldVersion }),但可以用 $eq 配合 $inc 在单次 write 中完成校验+升版,避免中间状态被篡改。
示例(Node.js + MongoDB Driver):
const result = await collection.updateOne(
{ _id: docId, version: 5 },
{
$set: { updatedAt: new Date(), /* 其他字段 */ },
$inc: { version: 1 }
}
);
注意点:
-
version必须是数字类型,不能是字符串或 ObjectId - 查询条件里的
version: 5是你上一步读到的值,不是硬编码 - 如果返回
matchedCount === 0,说明期间已被别人更新,当前操作失效 - 不要在
$set里再设version: 6—— 这会绕过原子性,且可能被并发写覆盖
为什么不用 findAndModify 或事务替代
findAndModify 确实能返回旧文档,但它不解决核心问题:你仍然得自己比对 version,而且它的原子性只限于单文档,没法加业务逻辑判断。事务则过度——乐观锁本意就是避免锁表,开事务反而引入写锁和性能开销。
更实际的取舍:
- 单文档更新场景,坚持用带
version条件的updateOne,轻量、快、兼容所有 MongoDB 版本 - 需要跨文档一致性?那已经超出乐观锁适用范围,该考虑业务层补偿或最终一致方案
- 如果频繁冲突(比如每秒多次写同一条),说明设计有问题——要么拆分粒度,要么换用分布式锁
容易被忽略的初始化和边界问题
第一次插入文档时,version 字段必须显式设为 0 或 1,不能留空或用 null。否则后续 $eq 匹配永远失败。
另外要注意驱动行为差异:
- MongoDB Node.js Driver v4+ 默认开启
strict模式,undefined字段不会被发送,但null会;建议统一用0初始化 - 如果用 Mongoose,记得关掉
versionKey: false,否则它会偷偷加__v并干扰你的逻辑 - 客户端时间不同步不影响 version 机制,但别拿
updatedAt当版本依据——它不可比较、不可递增
version 字段本身不索引也行,但如果查询频次高,建议给 { _id: 1, version: 1 } 加复合索引,避免全表扫。










