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

基于MongoDB的DDD应用并发更新冲突问题求解

基于DDD的MongoDB并发更新问题解决方案

问题背景

我正在设计一个基于DDD(领域驱动设计)的应用,使用MongoDB时遇到了并发更新问题。当前简化后的设计如下:

领域类 Character

public class Character : IEntity
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; private set; }

    public string FirstName { get; private set; }
    public string LastName { get; private set; }

    public Character(string name, string lastName)
    {
        Id = ObjectId.Empty.ToString();
        FirstName = name;
        LastName = lastName;
    }

    [BsonConstructor]
    public Character(string id, string name, string lastName)
    {
        Id = id;
        FirstName = name;
        LastName = lastName;
    }

    public virtual void UpdateName(string name)
    {
        FirstName = name;
    }

    public virtual void UpdateLastName(string lastName)
    {
        LastName = lastName;
    }
}

请求处理类 UpdateCharacterNameHandler

public class UpdateCharacterNameHandler : IUpdateCharacterNameHandler 
{
    private readonly ICharacterRepository _characterRepository;

    public UpdateCharacterNameHandler(ICharacterRepository characterRepository)
    {
        _characterRepository= characterRepository;
    }

    public async Task<Result<Character>> UpdateCharacterName(string id, string name)
    {
        var character = await _characterRepository.GetCharacterAsync(id);
        character.UpdateName(name);

        await _characterRepository.UpdateAsync(character);

        return Result.Ok(character);
    }
}

仓库的Update方法实现

public async Task<Character> Update(Character character)
{
    var filter = new FilterDefinitionBuilder<Character>()
        .Eq(x => x.Id, character.Id);

    return await _collection.FindOneAndReplaceAsync(
        filter,
        character,
        options:
            new FindOneAndReplaceOptions<Character, Character>
            {
                ReturnDocument = ReturnDocument.After
            });
}

当前实现无并发时正常运行,但多个请求同时读取Character初始状态并修改时,后续请求会覆盖之前的更新,因为无法感知数据库中的对象已被修改。我曾考虑在仓库中添加UpdateFirstName方法,但这样需要保持领域对象、处理类和仓库的同步(比如修改领域类的UpdateName方法时,需同步修改仓库方法),希望找到无需此类同步即可避免并发覆盖的方法。


解决方案

1. 乐观锁(版本号机制)

这是最贴合DDD思想的方案,无需破坏领域模型的封装性,也不需要同步仓库方法。

实现步骤:

  • 在Character类中添加版本号字段,由领域对象自身维护更新
  • 更新时,过滤器同时匹配Id和当前版本号,仅当版本匹配时执行替换
  • 领域方法修改状态时自动自增版本号

修改后的Character类:

public class Character : IEntity
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    // 添加版本号字段
    public int Version { get; private set; }

    public Character(string name, string lastName)
    {
        Id = ObjectId.Empty.ToString();
        FirstName = name;
        LastName = lastName;
        Version = 1; // 初始版本设为1
    }

    [BsonConstructor]
    public Character(string id, string name, string lastName, int version)
    {
        Id = id;
        FirstName = name;
        LastName = lastName;
        Version = version;
    }

    public virtual void UpdateName(string name)
    {
        FirstName = name;
        Version++; // 更新状态时自增版本号
    }

    public virtual void UpdateLastName(string lastName)
    {
        LastName = lastName;
        Version++;
    }
}

修改后的仓库Update方法:

public async Task<Character> Update(Character character)
{
    var originalVersion = character.Version - 1; // 匹配领域对象更新前的版本号
    var filter = new FilterDefinitionBuilder<Character>()
        .Eq(x => x.Id, character.Id)
        .Eq(x => x.Version, originalVersion);

    var result = await _collection.FindOneAndReplaceAsync(
        filter,
        character,
        options:
            new FindOneAndReplaceOptions<Character, Character>
            {
                ReturnDocument = ReturnDocument.After,
                IsUpsert = false // 仅更新已有文档,不允许插入
            });

    return result;
}

处理类添加并发冲突重试逻辑:

public async Task<Result<Character>> UpdateCharacterName(string id, string name)
{
    var retryCount = 3; // 设置重试次数
    while (retryCount > 0)
    {
        var character = await _characterRepository.GetCharacterAsync(id);
        character.UpdateName(name);

        var updatedCharacter = await _characterRepository.UpdateAsync(character);
        if (updatedCharacter != null)
        {
            return Result.Ok(updatedCharacter);
        }

        retryCount--;
        await Task.Delay(100); // 短暂延迟后重试
    }

    return Result.Failure<Character>("并发更新冲突,请稍后重试");
}

这种方式完全保留领域模型的封装性,所有状态变更都通过领域方法完成,仓库仅负责持久化,无需同步修改仓库方法。

2. 原子更新结合领域服务校验

如果不想引入版本号,可以使用MongoDB的原子更新操作,同时通过领域服务封装校验逻辑,避免仓库与领域类的强耦合。

定义领域服务封装规则:

public class CharacterDomainService
{
    public void ValidateFirstName(string firstName)
    {
        if (string.IsNullOrWhiteSpace(firstName))
        {
            throw new ArgumentException("FirstName不能为空");
        }
        // 可添加其他领域规则校验
    }
}

修改仓库方法:

public class CharacterRepository : ICharacterRepository
{
    private readonly IMongoCollection<Character> _collection;
    private readonly CharacterDomainService _domainService;

    public CharacterRepository(IMongoCollection<Character> collection, CharacterDomainService domainService)
    {
        _collection = collection;
        _domainService = domainService;
    }

    public async Task<Character> UpdateFirstName(string id, string newFirstName)
    {
        _domainService.ValidateFirstName(newFirstName);

        var update = new UpdateDefinitionBuilder<Character>()
            .Set(x => x.FirstName, newFirstName);

        var result = await _collection.FindOneAndUpdateAsync(
            x => x.Id == id,
            update,
            new FindOneAndUpdateOptions<Character, Character>
            {
                ReturnDocument = ReturnDocument.After
            });

        return result;
    }
}

处理类调用:

public async Task<Result<Character>> UpdateCharacterName(string id, string name)
{
    try
    {
        var updatedCharacter = await _characterRepository.UpdateFirstName(id, name);
        return Result.Ok(updatedCharacter);
    }
    catch (ArgumentException ex)
    {
        return Result.Failure<Character>(ex.Message);
    }
}

这种方式将领域规则集中在领域服务中维护,仓库仅负责执行原子更新和调用校验,无需同步修改多个组件。

3. 悲观锁(不推荐高并发场景)

MongoDB可通过findAndModify配合事务或maxTimeMS实现悲观锁,但会显著降低并发性能,仅适合低并发场景。不推荐在DDD架构中过度使用,会增加系统复杂度。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 16:21:11