在DDD& CQRS,正确设计同步读取模型与多个聚合更新

时间:2017-01-20 08:40:31

标签: domain-driven-design cqrs

假设我有2个聚合StaffShop,我有一个读取模型StaffModel,其中包含非规范化视图中的商店信息(shopId,名称,地址等)。

业务规则是在单个请求中创建StaffShop,因此我创建了一个CreateStaffService来创建一个Staff并触发StaffCreatedEvent,然后{ {1}}的{​​1}}听众创建CreateShopService,然后触发StaffCreatedEvent

在读模型方面,我有4种设计同步器服务的方法:

  1. 订阅Shop,创建ShopCreatedEvent的记录。然后订阅StaffCreatedEvent,使用staffId更新StaffModel上的商店信息。

  2. ShopCreatedEvent包含人员信息,同步服务订阅事件并一次性插入完整的读取模型。但是员工信息与StaffModel汇总无关,是否可以将其包含在事件中?

  3. 分别对模型ShopCreatedEventShop进行更新,以响应相应的汇总事件。

  4. 在单个交易中包裹StaffModelShopModel,触发CreateStaffService

  5. 我个人更喜欢选项2和4,与选项1一样,很难确保CreateShopService始终在StaffAndShopCreatedEvent之前到达。

    请分享您对此主题的想法和经验。 感谢

    更新

    为了避免使用序列号进行乱序事件消耗,假设我使用数据库序列生成序列号,每次递增1,然后假设我的订阅者使用事件1,因此最后处理的事件序列为1然后,生产者按顺序发布事件2,事件3和事件4,并且仅在当前事务成功时发送事件。因此,如果事务2创建序列号2,但事务失败并回滚,则不会发送事件2,但事件3和事件4已成功发送。

    在消费者方面,事件3和事件4都比上次处理的事件1更新,事件2永远不会到来。因此,在这种情况下检查StaffCreatedEvent是错误的,除非事件序列号(版本)是严格顺序的,这也很难保证。

1 个答案:

答案 0 :(得分:3)

您已经定义了两个不同的聚合,这些聚合也可以在它们自己的有界上下文中。对于事件,每个事件应描述单个聚合。您将看到的大多数文档和示例都显示了单一的聚合标识符。请记住,当你正在观察你现在的事件的创造性方面时,它仍然只是一个三角形 - 在这种情况下来自"没有"到"某事"。

许多框架都涉及您的主要关注点,即处理乱序事件。这是一个真正的问题,特别是对于分布式系统。为了缓解这种情况,通常在写入侧给事件提供顺序标识符,然后按顺序向非规范化器提供事件。因此,如果事件4到达,并且只处理了事件2,它将保持事件4,直到事件3处理完毕。

以下是围绕同一问题的一般性讨论:Handling out of order events in CQRS read side

听起来你正在推动自己的框架,这可能令人生畏。我正在做同样的事情,但它更多的是扩展知识而不是计划在现实环境中使用。然而,我可以为您提供的是考虑如何让事件更加可预测地到达。如果您现在没有考虑扩展,那么您可以通过确保将事件推入FIFO队列来缓解您的大量关注。然后,您的同步服务可以轮询队列,而不是订阅事件。在重建聚合时,可以通过对事件进行排序来重放,并且您有一个很好的起点。这样做,除非你有多个进程轮询你的队列,否则你实际上不必担心乱序事件。

为了确保您实际按顺序生成事件,您所描述的内容听起来像是域服务的一个很好的用例。您正在协调两个聚合的操作。从域服务中提升这些事件有助于确保两个聚合上的操作都已完成。

<强>更新

我会稍微扩展一下以反映其他问题。让我们退后一步吧。在写入方面,您永远不会持久化您的聚合。您将持久化反映状态更改的事件,包括创建。您的目标是发出命令,命令将被赋予聚合,聚合将从命令创建一个事件。创建事件后,聚合将其应用于自身,然后使其可供命令处理程序从聚合中检索。由于该事件是聚合需要知道改变状态的所有事件,因此我们将不得不将其保存到写入端。当我们再次需要该聚合时,我们只需加载与给定聚合标识符相关的所有事件,并将它们与聚合类重放。一旦重播了所有事件,就可以为你的聚合物提供水分。

当您的命令处理程序将事件传递给您的事件存储时,它只关心它是一个事件。在持久性方面,您可能会在特定字段中序列化事件,以及包含事件周围元数据的其他字段;例如聚合标识符和聚合类型(当您需要重放时,这使得查询更容易)。此外,该事件将具有顺序标识符。这是自动增量标识符的好地方。而且,非常重要的是,你在那里有连续性。

对于你正在做的事情的最大部分,你最终一次只能插入一个事件。它很可能构成您运营的大部分。优雅的是,几乎没有失败的机会。由于事件只是一个三角洲的流,所以没有参考完整性。您可能会遇到更复杂的情况,主要是通过使用传奇或流程管理器,您可以在将事件发布到事件存储之前维护多个聚合的状态。你如何做到这一点可以从简单到复杂。但是,您的主要困难在于确保聚合项未被流程管理器/传奇范围之外发生的其他事件所突变。但这远远超出了你要求的范围。在结束时,我将提供一个很好的资源来阅读。

返回事件流。由于您只是将事件推送到写入端,并且我们可以假设它们是按顺序创建的,因此当您进行预测时,可能会出现无序事件。最常见的情况是,这将发生在扩展部署中,您可以在多个服务器中从队列中挑选命令并分发它们。在您的事件存储将事件推送到写入端以实现持久性之后,它会将其推送到事件总线,您的订阅者将对其进行操作。这是订购变得重要的地方,因为这是您的阅读方将更新的地方。

以你的情况为例,让我们说事件1正在创建员工,事件2正在创建商店。如果他们以相反的顺序命中事件总线,你需要知道。事件总线的目标是将事件传递到某个地方,这通常是一个队列。此时,一个简单的FIFO队列实际上会出现问题,因为您处于模仿接收顺序的情况。您的排队机制应该能够检查所有排队的项目,并且您需要跟踪上次处理的事件。在处女系统上,有两个请求,监视队列的任何事情都知道最后处理的事件是0.它看到2到达并且什么都不做。然后1到达,它可以将事件1推送给所有订户,将跟踪上次处理的事件的数值更新为1,调度事件2,并将内部跟踪器更新为2.基本上,您正在查看某种服务监视事件总线发布到的队列,然后对每个事件的订户进行显式调度。

我曾提到过一个涵盖了很多内容的资源,那就是Microsoft's CQRS Journey,这是他们的模式和范围的一部分。实践系列。即使您不在Microsoft堆栈上,它也有很多很棒的信息。最好的部分是它真的是一个“旅程”#34;。您不仅可以获得代码示例以及为什么要这样做,而是可以在准现实世界的应用程序中看到项目的演变。整本书可以免费下载,PDF格式。如果您愿意,也可以获得纸质书。 (我做了,因为有一些关于科技书籍的触觉感觉以及我发现自己翻阅它们的方式,这些都不利于电子书)。