You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决MongoDB文档读写并发冲突?附多线程场景示例

场景说明

将Address文档ID嵌入User文档中,线程1、线程2同时执行,之后执行结果检查线程。

User文档初始状态

addresses: [1, 2, 3]

异步线程1操作

user = userCollection.find(userId)
user.addresses.push(4)
userCollection.update(user)

异步线程2操作

user = userCollection.find(userId)
user.addresses.push(5)
userCollection.update(user)

结果检查

user = userCollection.find(userId)
user.addresses.includes(4) // 结果不确定
user.addresses.includes(5) // 结果不确定
问题与解答

问题1:是否需要在应用层实现自定义文档级锁,防止多线程应用覆盖其他线程正在写入的数据?

不需要优先在应用层实现自定义文档级锁。主流数据库本身提供的原子操作和乐观锁机制,能更高效解决这类并发覆盖问题:

  • 乐观锁:给User文档添加version字段,每次更新时带上当前版本号,仅当版本匹配时更新成功。比如Golang操作MongoDB时,用UpdateOne配合bson.M{"_id": userId, "version": currentVersion}作为过滤条件,更新同时自增version。若更新失败(匹配数为0),说明已有其他线程修改文档,此时重试操作即可。
  • 原子更新操作:像示例中的数组追加,直接用数据库原生原子命令(如MongoDB的$push),跳过先查询再更新的步骤,从根源避免竞态覆盖。

应用层自定义锁会额外引入锁过期、分布式同步等复杂度,仅当数据库原生机制无法满足需求时,再考虑这类方案。

问题2:除数组原子追加操作外,针对查找删除旧ID、同时其他线程添加新ID的复杂场景,最简单的解决方案是什么?

最简单的方案是利用数据库的原子复合操作,将删除旧ID和添加新ID的逻辑合并为一个原子执行的数据库命令,避免先查询再修改的竞态。

以Golang操作MongoDB为例,若要删除旧ID2并添加新ID6,可直接用UpdateOne执行包含$pull和$push的复合更新:

filter := bson.M{"_id": userId}
update := bson.M{
    "$pull": bson.M{"addresses": 2},
    "$push": bson.M{"addresses": 6},
}
result, err := userCollection.UpdateOne(context.TODO(), filter, update)

该复合操作会在数据库端原子执行,无论其他线程同时执行何种添加操作,都不会出现删除与添加的竞态问题。

如果是关系型数据库(如MySQL),可通过UPDATE语句结合子查询直接在SQL中完成原子逻辑,或用事务包裹操作(注意设置合适的事务隔离级别)。

内容的提问来源于stack exchange,提问作者fIwJlxSzApHEZIl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 23:19:01