同一限界上下文内命令库与查询库同步及事务处理咨询
首先,咱们先拆解你当前代码的核心问题:你的事务装饰器只管控了NHibernate的写端事务,但EF查询端的操作是脱离这个事务独立执行的——也就是说,当你在命令处理器里调用_queryService.Create时,EF大概率已经直接执行了SaveChanges把读模型写入数据库,而NH的写模型要等到装饰器里的Commit才会落地。如果此时NH的提交失败(比如数据库约束冲突、异常),读模型已经成功保存,写模型却被回滚,这就导致了你看到的不一致现象。
下面给你两种针对性的解决方案,分别适配不同的业务场景:
方案一:领域事件+Outbox模式(推荐,符合DDD设计原则)
DDD的核心思想是让命令端专注于业务规则和聚合根修改,读写模型的同步应该通过领域事件异步完成,而不是在命令处理里直接耦合读操作。这种方式既能解耦读写逻辑,又能保证最终一致性,还避免了分布式事务的复杂度。
具体步骤:
给聚合根添加领域事件
在Category聚合根里定义创建完成的领域事件,确保事件和聚合根的状态变更绑定:public class Category : AggregateRoot { // 原有属性... public List<IDomainEvent> DomainEvents { get; private set; } = new(); public Category(CategoryId id, string name, CategoryId parentId) { // 原有初始化逻辑... // 添加领域事件,记录创建信息 DomainEvents.Add(new CategoryCreatedDomainEvent(id.Value, name, parentId.Value)); } } // 定义领域事件 public class CategoryCreatedDomainEvent : IDomainEvent { public Guid CategoryId { get; } public string Name { get; } public Guid ParentId { get; } public CategoryCreatedDomainEvent(Guid categoryId, string name, Guid parentId) { CategoryId = categoryId; Name = name; ParentId = parentId; } }修改命令处理器,移除读模型直接调用
命令端只负责业务校验和聚合根持久化,不再直接操作读模型:public void Handle(CreateCategoryCommand command) { var categoryId = new CategoryId(Guid.NewGuid()); var parentId = new CategoryId(command.ParentId); var category = new Category(categoryId, command.Name, parentId); var parent = _repository.GetById(parentId); if (parent == null) throw new ParentCategoryNotFoundException(); _repository.Create(category); // 移除_queryService.Create调用,交给领域事件处理 }实现事件持久化与消费(Outbox模式)
- 在NH的UnitOfWork提交时,把聚合根的领域事件保存到Outbox表(和写模型同库同事务),确保写模型提交成功时事件才会被持久化;
- 用后台任务(比如Hangfire、Quartz)定期读取Outbox表中的未处理事件,调用EF查询服务更新读模型;
- 如果写模型事务回滚,Outbox里的事件也会被回滚,读模型就不会被错误更新。
方案二:分布式事务(强同步场景可选)
如果你的业务要求读写模型必须实时强一致,可以用分布式事务同时管控NH和EF的数据库操作,但要注意这种方案会带来性能开销和运维复杂度,仅适合必须强同步的场景。
具体步骤:
扩展UnitOfWork,同时管理NH和EF的连接
实现一个复合UnitOfWork,绑定两个数据库的连接到同一个分布式事务:public interface ICompositeUnitOfWork : IUnitOfWork { ISession NhSession { get; } DbContext EfDbContext { get; } } public class CompositeUnitOfWork : ICompositeUnitOfWork { private readonly ISession _nhSession; private readonly DbContext _efDbContext; private IDbTransaction _distributedTransaction; public CompositeUnitOfWork(ISession nhSession, DbContext efDbContext) { _nhSession = nhSession; _efDbContext = efDbContext; } public ISession NhSession => _nhSession; public DbContext EfDbContext => _efDbContext; public void Begin() { // 基于NH连接开启分布式事务 var nhConnection = _nhSession.Connection; _distributedTransaction = nhConnection.BeginTransaction(IsolationLevel.ReadCommitted); // 将EF上下文绑定到同一事务 _efDbContext.Database.UseTransaction(_distributedTransaction); } public void Commit() { _distributedTransaction.Commit(); } public void Rollback() { _distributedTransaction.Rollback(); } }修改事务装饰器,使用复合UnitOfWork
确保事务同时管控NH和EF的操作:public class TransactionalCommandHandlerDecorator<T>:ICommandHandler<T> { private readonly ICommandHandler<T> _commandHandler; private readonly ICompositeUnitOfWork _unitOfWork; public TransactionalCommandHandlerDecorator(ICommandHandler<T> commandHandler, ICompositeUnitOfWork unitOfWork) { _commandHandler = commandHandler; _unitOfWork = unitOfWork; } public void Handle(T command) { _unitOfWork.Begin(); try { _commandHandler.Handle(command); // 先保存EF的变更(不提交,由分布式事务统一处理) _unitOfWork.EfDbContext.SaveChanges(); // 统一提交分布式事务 _unitOfWork.Commit(); } catch (Exception exp) { _unitOfWork.Rollback(); throw; } } }调整查询服务,延迟SaveChanges
让查询服务不再直接调用SaveChanges,由复合UnitOfWork统一处理:public class CategoryQueryService : ICategoryQueryService { private readonly AppDbContext _dbContext; public CategoryQueryService(AppDbContext dbContext) { _dbContext = dbContext; } public void Create(CategoryQuery queryModel) { _dbContext.CategoryQueries.Add(queryModel); // 移除这里的SaveChanges,交给CompositeUnitOfWork统一提交 } }
总结建议
优先选择领域事件+Outbox模式,这更贴合DDD的设计理念,解耦读写逻辑,降低系统复杂度;只有当业务要求必须实时强一致时,再考虑分布式事务方案。
内容的提问来源于stack exchange,提问作者Afsaneh Daneshi

