MongoDB是否有等价@DynamoDBVersionAttribute的乐观锁、条件写入机制?
MongoDB 原生提供与DynamoDB乐观锁、条件写入完全对等的能力,无需额外依赖即可解决并发写入互相覆盖导致的数据丢失问题。
原生条件写入(等价DynamoDB条件写入能力)
MongoDB 所有单文档写入操作(updateOne/replaceOne/findOneAndUpdate/deleteOne等)均支持在查询筛选阶段加入任意校验规则,仅当文档完全匹配筛选条件时写入才会生效,不匹配则直接返回匹配失败,不会执行写入逻辑。
最基础的用法是将更新前读取到的旧值作为筛选条件,避免无感知覆盖:
// 仅当账户当前余额确实为100时,才将余额扣减为80 db.accounts.updateOne( { userId: "u123", balance: 100 }, { $set: { balance: 80 } } )
如果接口返回的matchedCount为0,说明从你读取旧值到提交写入的间隙,文档已经被其他请求修改,此时可以重新读取最新值完成业务校验后重试,不会出现后序请求直接覆盖前序写入结果的问题。
版本号乐观锁(等价DynamoDB乐观锁实现)
如果不想每次写入都校验全量业务字段,可以直接复用DynamoDB的版本号乐观锁逻辑,MongoDB原生支持该模式:
- 每个文档新增
version字段,类型为长整型,文档创建时初始值设为0 - 每次更新文档时遵循两个规则:
- 筛选条件必须带上本次读取到的
version值 - 更新时通过
$inc操作符将version字段自增1
示例代码:
- 筛选条件必须带上本次读取到的
// 假设更新前读取到文档的version值为3 db.articles.updateOne( { _id: "a456", version: 3 }, { $set: { content: "更新后的文章内容" }, $inc: { version: 1 } } )
如果返回的matchedCount为0,说明期间已有其他请求完成更新、版本号已发生变化,当前写入不会生效,可根据业务场景选择重试或返回冲突提示。
注意:不要使用客户端生成的时间戳作为版本校验依据,分布式场景下时钟偏移会导致校验失效,使用自增整型版本号的稳定性最高。
原子操作符从根源避免覆盖
如果你的更新逻辑不是全量替换文档,而是做字段级的增量修改,优先使用MongoDB内置的原子更新操作符,从根源上消除读改写流程带来的覆盖风险:
- 数值增减操作使用
$inc,不要先读文档修改数值后再全量写回 - 数组增删操作使用
$push/$pull,不要读取全量数组修改后整体写回 - 嵌套字段修改直接通过点路径定位目标字段更新,不要读取整个文档修改后全量替换
正反写法对比如下:
// 错误写法:先读再全量替换,并发场景下极易出现写入覆盖 const doc = db.coll.findOne({_id: "x"}) doc.viewCount += 1 db.coll.replaceOne({_id: "x"}, doc) // 正确写法:原子字段更新,不存在覆盖风险 db.coll.updateOne({_id: "x"}, {$inc: {viewCount: 1}})
一致性说明
MongoDB单文档级别的写入具备原子性保证,只要写入时携带匹配校验条件,就不会出现无感知的写入覆盖问题,一致性等级与DynamoDB的条件写入、乐观锁能力完全对等。
内容的提问来源于stack exchange,提问作者Dave

