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

使用MongoDB Upsert与乐观并发(版本控制)时如何避免DuplicateKeyException?

带版本控制的MongoDB乐观并发Upsert问题

我正在使用MongoDB的.NET驱动,尝试通过UpdateOneAsync和IsUpsert = true实现带版本控制的乐观并发模式。

现有代码

public Task UpsertAsync(User user, CancellationToken cancellationToken) =>
    context.Users.UpdateOneAsync(
        Builders<User>.Filter.Eq(u => u.Id, user.Id) &
        Builders<User>.Filter.Lt(u => u.Version, user.Version),
        Builders<User>.Update
            .SetOnInsert(u => u.Id, user.Id)
            .Set(u => u.IsDeleted, user.IsDeleted)
            .Set(u => u.Version, user.Version)
            .Set(u => u.Name, user.Name)
            .Set(u => u.UserName, user.UserName)
            .Set(u => u.Media, user.Media),
        new UpdateOptions { IsUpsert = true },
        cancellationToken
    );

目标

  • 文档不存在时执行插入操作
  • 仅当传入的Version大于存储版本时才执行更新

问题

当文档已存在且存储版本更高时,过滤器条件Id == user.Id AND Version < user.Version不匹配,此时因为IsUpsert = true,MongoDB会尝试插入新文档,最终引发DuplicateKeyException(Id重复)。

疑问

处理该场景的推荐方式是什么?

  • 捕获并忽略DuplicateKeyException是否是该模式下的正确/预期做法?
  • 有没有更好的方式实现“仅版本更新时执行更新,否则不操作”且不触发插入尝试?

我希望保持操作原子性并避免竞态条件,因此优先选择单查询解决方案。


更新补充

  • 文档存在且传入版本更新 → 执行更新
  • 文档存在且传入版本更旧 → 忽略操作
  • 文档不存在 → 执行插入

我的场景中大多数操作是插入(文档尚未存在),版本更新操作相对较少。我正在实现CQRS模式,包含命令和查询两个微服务。命令侧已采用“读取→更新”的乐观并发流程,冲突时会抛出并发异常。

提出此问题是针对查询侧的特殊边缘情况:“UserCreated”事件可能在“UserUpdated”事件之后被处理。不过通常事件会按顺序处理——先处理“UserCreated”事件,再按版本顺序处理“UserUpdated”事件。我的核心目标是保持操作原子性,理想情况下使用单查询且不使用事务。


解决方案

方案1:调整过滤器逻辑,避免不必要的插入尝试

要实现目标中的原子性单查询逻辑,可以修改过滤器,让插入操作仅在文档不存在时触发,而非过滤器不匹配时都尝试插入。

具体是用$or组合两个互斥条件:

  1. 文档存在,且存储版本小于传入版本(满足更新要求)
  2. 文档不存在(满足插入要求)

修改后的代码如下:

public Task UpsertAsync(User user, CancellationToken cancellationToken) =>
    context.Users.UpdateOneAsync(
        Builders<User>.Filter.Or(
            // 条件1:文档存在且版本低于传入版本,执行更新
            Builders<User>.Filter.And(
                Builders<User>.Filter.Eq(u => u.Id, user.Id),
                Builders<User>.Filter.Lt(u => u.Version, user.Version)
            ),
            // 条件2:文档不存在,执行插入
            Builders<User>.Filter.Not(Builders<User>.Filter.Exists(u => u.Id))
        ),
        Builders<User>.Update
            .SetOnInsert(u => u.Id, user.Id)
            .Set(u => u.IsDeleted, user.IsDeleted)
            .Set(u => u.Version, user.Version)
            .Set(u => u.Name, user.Name)
            .Set(u => u.UserName, user.UserName)
            .Set(u => u.Media, user.Media),
        new UpdateOptions { IsUpsert = true },
        cancellationToken
    );

该方案的优势:

  • 完全原子性,单查询完成所有逻辑
  • 避免版本冲突时的插入尝试,不会抛出DuplicateKeyException
  • 完美匹配你的三个业务规则:存在且版本更新则更新,存在且版本旧则忽略,不存在则插入

方案2:捕获并忽略DuplicateKeyException(不推荐)

如果坚持原有过滤器逻辑,捕获并忽略DuplicateKeyException确实能解决报错问题,但存在明显缺点:

  • 异常处理会增加额外性能开销,高并发场景下影响更明显
  • 无法区分是版本冲突导致的插入失败,还是其他业务逻辑错误引发的Id重复问题
  • 属于被动处理错误,而非主动避免不必要的插入尝试

因此方案1是更优的选择。


内容的提问来源于stack exchange,提问作者Muhammed Furkan Güler

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 12:28:14