基于DDD、Event Sourcing与CQRS的事件存储时机问询
问题描述
我正在基于DDD、Event Sourcing和CQRS构建系统,配置了两个数据库和两个Entity Framework上下文:一个用于存储规范化数据,另一个用于存储事件。
我的聚合根代码如下:
public class Order : Entity, IAggregateRoot { private Order() { this.AddDomainEvent(new OrderCreatedDomainEvent(this)); } public static Order CreateOrder => new Order(); }
同时用MediatR实现了创建订单的命令处理器:
public class CreateOrderCommandHander : ICommandHandler<CreateOrderCommand> { private readonly IOrderRepository _orderRepository; public async Task<Unit> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { // 创建包含OrderCreatedDomainEvent领域事件的新订单 var order = Order.CreateOrder(); this._orderRepository.AddOrder(order); // 保存创建的订单 await this._orderRepository.UnitOfWork.SaveChangesAsync(); //... } }
我的疑问是:OrderCreatedDomainEvent具体何时触发并被存入事件存储?是在通过IOrderRepository将新记录添加并保存到规范化数据库之前还是之后?我猜测是在之前,因为事件存储是系统的唯一可信源,必须始终包含系统的最新状态;若先写入规范化数据库,事件存储会出现数据不一致。另外,我看到有示例中会在保存规范化数据库数据前分发领域事件,这是正确的实现方式,还是仅为可行方案之一?
回答
好问题!这其实是Event Sourcing实现中非常关键的一致性问题,咱们一步步拆解来看:
1. 事件存储的核心地位
首先你说得没错——事件存储是Event Sourcing架构下的唯一可信数据源,所有状态变更都必须以事件的形式先被持久化,规范化数据库(也就是你说的读库)的数据只是事件的投影结果。所以从原则上来说,事件必须在规范化数据之前被写入,这样才能避免“写了读库但事件没保存”这种不一致情况。
2. 你的代码中事件的触发时机
在你的Order聚合根构造函数里,你调用了AddDomainEvent(new OrderCreatedDomainEvent(this))——这一步只是把事件添加到聚合根的内存事件集合里,并没有真正持久化到事件存储。真正的持久化时机完全取决于你的IOrderRepository和UnitOfWork的实现逻辑:
- 如果你的
SaveChangesAsync()方法内部是先把聚合根的所有领域事件写入事件存储,再保存规范化数据到读库,那这符合Event Sourcing的最佳实践; - 如果反过来先保存读库再写事件,那确实存在风险:比如读库写入成功但事件存储写入失败,这时候系统就会出现“读库有订单但事件记录缺失”的不一致,后续重建聚合根状态就会出错。
3. 关于“Commit前分发事件”的方案
你提到的“在保存规范化数据库前分发领域事件”其实是标准实现方式之一,但更准确的描述应该是:在同一个事务边界内,先持久化事件到事件存储,再更新读库(或者触发投影更新)。这里的关键是要保证这两个操作在同一个事务里吗?不一定——如果你的事件存储和读库是不同的数据库(像你这样双DB配置),分布式事务可能会有性能问题,这时候通常会用“最终一致性”的方案:
- 先将事件写入事件存储(这一步必须成功);
- 然后通过事件订阅(比如MediatR的事件发布、或者消息队列)触发读库的投影更新;
- 如果投影更新失败,通过重试机制或者补偿操作来保证最终一致。
而如果你的UnitOfWork能把事件存储和读库的操作放在同一个本地事务里(比如用同一个数据库实例的不同 schema),那当然更好,能做到强一致性。
总结
- 事件的持久化必须早于规范化数据的写入(或者至少在同一个事务边界内),这样才能保证事件存储作为可信源的地位;
- “保存读库前分发/持久化事件”是正确的实现方向,具体是否用事务、是否用最终一致性,取决于你的系统性能和一致性要求。
内容的提问来源于stack exchange,提问作者OverflowStack

