如何处理多个NodeJS请求同时读写同一份MongoDB文档的并发问题
问题本质
你遇到的是MongoDB单文档*读写竞态(丢失更新)*问题:单文档的读、写是两个独立操作,并发请求会在更新操作执行前全部拿到相同的旧文档快照,后提交的更新会直接覆盖前面的更新结果,最终只有最后一次更新生效。
解决方案(按优先级从高到低排序)
方案1:使用MongoDB原子操作(首推)
MongoDB单文档写操作本身是原子性的,直接把计算逻辑放到数据库侧执行,完全规避先读后写的竞态问题。
简单的加减乘除计算可以直接用内置操作符实现,例如给score字段加10:db.yourCollection.updateOne({_id: 目标文档ID}, {$inc: {score: 10}})
复杂计算可以用MongoDB 4.2及以上版本支持的更新聚合管道实现,例如基于文档已有字段price和count计算打8折后的total:db.yourCollection.updateOne( { _id: 目标文档ID }, [ { $set: { total: { $multiply: ["$price", "$count", 0.8] } } } ] )该方案无额外开发成本、性能最高,能覆盖90%以上的业务场景。
方案2:乐观锁实现
如果你的计算逻辑非常复杂,无法通过MongoDB内置操作/聚合管道实现,可通过版本号实现乐观锁:- 给业务文档新增
version字段,初始值为0 - 读取文档时同时拿到当前
version值 - 本地完成计算后执行更新,更新条件加上
version匹配,同时将version加1:
const updateResult = await db.yourCollection.updateOne( { _id: 目标文档ID, version: 读取到的version值 }, { $set: { 要更新的字段: 计算结果 }, $inc: { version: 1 } } )- 判断
updateResult.modifiedCount,如果为0说明中间已有其他请求更新了文档,重试整个读-计算-写流程即可。
该方案实现简单,不需要额外依赖,适合并发量中等的场景。
- 给业务文档新增
方案3:分布式锁实现
如果业务并发量极高,乐观锁的重试成本不可接受,可以引入分布式锁保证同一时间只有一个请求处理目标文档的读写逻辑。你可以用Redis或者MongoDB单独的锁集合实现原子抢锁逻辑,抢锁成功后再执行业务操作,执行完成后释放锁即可。
内容的提问来源于stack exchange,提问作者gorgcow
相关产品推荐
相关产品推荐

