如何处理Node.js批量更新MongoDB时的并发冲突?

作者:袖梨 2026-08-10

updateMany在高并发下会丢更新,因其无版本校验、纯覆盖写:两请求同时读库存10并各减1后均写回9,实际应为8;需用findOneAndUpdate配合version字段实现乐观锁,且version必须为数字、条件精确匹配、更新含$inc、returnDocument设为'after'。

updateMany 为什么会在高并发下丢更新?

不是 updateMany 本身有问题,而是默认行为不防冲突:它只管“找到就改”,不检查当前值是否已被其他请求覆盖。比如两个请求同时读到库存为 10,各自减 1 后都写回 9,实际应为 8——这就是典型的“丢失更新”。现象是业务数据对不上、状态错乱,但 MongoDB 不报错,静默失败。

根本原因在于 updateMany 没有版本控制或条件约束,纯靠“覆盖写”。它适合后台批量同步这类无状态场景,但不适合订单、库存、计数器等需强一致性的并发更新。

  1. 别指望 writeConcern 或 timeoutMS 能解决这个问题——它们只管写入是否成功,不管逻辑是否正确
  2. 事务也救不了:updateMany 在事务里照样不带版本校验,且事务加锁反而放大 WriteConflict(错误码 112)
  3. findOneAndUpdate 是原子操作,但它只处理单文档;updateMany 天然不支持 per-document 条件判断

用 findOneAndUpdate + version 字段实现乐观锁

这是单文档并发更新最轻量、最可靠的方案。核心是把 version 当作“数据快照指纹”,每次更新都带上旧 version 值,MongoDB 只有在匹配时才执行,否则 matchedCount === 0。

关键点必须全部满足,缺一不可:

  1. version 字段插入时必须初始化为 01不能是 null、undefined 或字符串 "1"
  2. 查询条件必须显式写成 { _id: id, version: oldVersion },不能用 $ne: null 这类宽松匹配
  3. 更新操作必须包含 $inc: { version: 1 },不能手动 $set: { version: 2 }——否则破坏原子性
  4. Node.js 驱动 4.x+ 默认 returnDocument: 'before',重试时拿不到新 version,务必显式设为 returnDocument: 'after'

示例:

const result = await collection.findOneAndUpdate({ _id: docId, version: currentVersion },{ $set: { status: 'paid' }, $inc: { version: 1 } },{ returnDocument: 'after', upsert: false })

批量更新多个文档时怎么避免冲突?

如果你真要一次改几百条(比如发优惠券、批量改状态),又要求每条都防冲突,updateMany 就不合适了。得退回到逐条 + 有限并发:

  1. for...of 串行执行 findOneAndUpdate,最稳,适合中小批量(
  2. 若要并发,用 Promise.allSettled() 控制最多 3–5 个并发批次,别用 Promise.all()——一个失败全崩
  3. 每批后加 await new Promise(r => setTimeout(r, 5)),缓解服务端压力,降低 WiredTiger 锁竞争
  4. 别提前把所有文档 load 到内存再循环——用 cursor.toArray() 分页拉取,防 OOM

注意:bulkWriteupdateOne 也支持 version 条件,但返回结果是批量统计,你无法知道哪一条因 version 不匹配而跳过——除非你后续再查一遍,成本高。

什么时候该放弃乐观锁,改用 findAndModify 或队列?

当你的更新逻辑涉及外部依赖(比如调用支付网关、发短信),或者 version 字段已存在但类型不规范(如字符串型 version),乐观锁就容易失效。这时更稳妥的做法是:

  1. findAndModify(即 findOneAndUpdate 的底层命令),它保证读写原子,且支持 new: true 直接返回新值
  2. 把更新请求推入 Redis 队列,用单消费者串行处理——牺牲一点延迟,换确定性
  3. 如果业务允许最终一致性,改用 change stream 监听变更,异步补偿,而不是强求实时更新

真正容易被忽略的是:version 字段的 BSON 类型。MongoDB 对 "1"1 的比较规则完全不同,线上出过太多因字符串 version 导致的“看似成功、实则没更新”的坑。每次插入前用 Number() 显式转一下,比事后 debug 强十倍。

相关文章

精彩推荐