域事件与事件源和CQRS的因果依赖性

时间:2014-05-20 00:21:38

标签: domain-driven-design cqrs event-sourcing causality akka-persistence

我们假设我们有一个生成两个事件的写模型(域):

  • CarrierAdded(...)
  • BusConnectionCreated(carrier,...)

Carrier和BusConnection类是(部分)单独的聚合。 BusConnection被分配给Carrier并包含其CarrierId(单独的聚合仅由id引用)。

在正常的命令和事件流中,写入模型和读取模型都很好,但是当我们想要从头开始重建/添加新的读取模型时,问题出现了。

许多人建议(例如akka-persistence library)在事件存储中按事件存储事件。当反规范器要求回复事件时,他从每个聚合中获得两个独立的事件流。问题是来自上述示例中的不同聚合的某些事件需要按照它们添加到事件存储的相同顺序进行回复。这意味着我们需要某种因果依赖/部分排序。

最后我的问题:

  • 我应该重新考虑我的域名设计(错误的聚合边界?)或
  • 我是否只需要执行部分订购?

如果是后者,那么最有效的方法是什么?

  • 全球专柜?似乎没有可扩展性。
  • 某种矢量时钟?
  • 当它们出现时会在非规范化程序中检测到这些问题?例如。我们收到了CarrierId,我们还没有使用此ID的CarrierAdded活动,所以我们将事件存入并等待预期的事件
  • 在重播模式下处理事件时引入一些顺序?例如。有关运营商的所有事件,首先是BusConnection相关事件?

1 个答案:

答案 0 :(得分:6)

不,IMO你的设计非常精致且很常见。你必须以某种方式强制执行部分排序。

我不熟悉akka-persistence,但有些事件存储通过记录事件的时间戳来实现部分排序,并按照时间戳的顺序重放事件。一些事件存储确实使用全局计数器,例如当底层数据库是关系数据库且系统不需要扩展时 - 数据库提供的序列号在这里工作正常。

时间戳当然只能保证订购达到给定的粒度,通常为1ms。在某些情况下,这已经足够了,例如,如果您可以确保CarrierAdded 从不在与BusCarrierAdded相同的毫秒内发生。

至于可扩展性,确保横向扩展确实是您的问题,公交车运输系统似乎并不是“大规模”......如果您可以使用单个主数据库服务器,事件流中事件的序列号是实现所需部分排序的直接且可靠的方式。对于"交织" /并行提交,序列号可能会失败,因此您需要确保在BusCarrierAdded之前始终将CarrierAdded事件添加到事件存储/数据库,但这似乎可能在您的方案中。

如果确实需要横向扩展且两个事件都可能出现在同一毫秒内,那么您确实必须检测处理程序中的问题并使用非规范化。这听起来比它更难,因为它可以通过一个非常简单的状态机轻松实现。 Jonathan Oliver在传奇语境中有blogged about state matchines here,但这个原则也适用于此。