如何解决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
相关产品推荐
相关产品推荐

