基于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
相关产品推荐
相关产品推荐

