在事件源系统中汇总聚合关系

时间:2018-03-14 21:51:57

标签: domain-driven-design cqrs event-sourcing

所以我试图弄清楚CQRS + ES架构的一般用例背后的结构,我遇到的问题之一是如何在事件存储中表示聚合。如果我们将事件划分为流,那么流究竟代表什么?在一个跟踪项目集合的假设库存管理系统的背景下,每个项目都有ID,产品代码和位置,我无法直观地看到系统的布局。

从我在互联网上收集到的内容,可以简洁地描述“每个聚合一个流”。所以我会有一个Inventory聚合,一个包含ItemAdded,ItemPulled,ItemRestocked等事件的事件,每个事件都包含序列化数据,包含Item ID,数量变化,位置等。聚合根将包含一个InventoryItem对象的集合(每个对象都带有它们各自的数量,产品代码,位置等等。这似乎可以轻松实施域规则,但我看到了一个主要的缺陷;将这些事件应用于聚合根时,您必须首先重建该InventoryItem集合。即使使用快照,对于大量项目而言效果似乎也非常低效。

另一种方法是每个InventoryItem有一个流跟踪所有仅与item有关的事件。每个流都使用该项的ID命名。这似乎是更简单的路线,但现在您将如何强制执行域规则,例如确保产品代码是唯一的,或者您不是将多个项目放在同一位置?看起来你现在必须引入一个Read模型,但这不是保持命令和查询分离的重点吗?这只是感觉不对。

所以我的问题是'哪个是正确的?'部分两个?都不是?像大多数事情一样,我学的越多,我学到的就越多,我不知道......

2 个答案:

答案 0 :(得分:2)

  

所以我试图弄清楚CQRS + ES架构的一般用例背后的结构,我遇到的问题之一是如何在事件存储中表示聚合

DDD项目中的事件存储是围绕事件源聚合设计的:

  1. 它提供了以前由聚合根实例(具有给定的指定ID)发出的所有事件的高效加载
  2. 必须按发出的顺序检索这些事件
  3. 它不允许同时为同一聚合根实例附加事件
  4. 由于单个命令而发出的所有事件必须全部以原子方式附加;这意味着他们都应该成功或全部失败
  5. 第4点可以使用交易来实现,但这不是必需的。实际上,出于可伸缩性的原因,如果可以,那么您应该选择一种持久性,它可以在不使用事务的情况下为您提供原子性。例如,您可以将事件存储在MongoDB文档中,因为MongoDB可以保证文档级的原子性。

    第3点可以使用乐观锁定来实现,使用version列,每列具有唯一索引(版本x AggregateType x AggregateId)。

    同时,有一个关于聚合的DDD 规则:不要在每个事务中改变多个聚合。此规则可帮助您设计可扩展的系统。如果你不需要,请打破它。

    因此,所有这些要求的解决方案都称为事件流,它包含Aggregate实例以前发出的所有事件。

      

    所以我会有一个库存聚合

    DDD的优先级高于Event-store。因此,如果您有一些业务规则迫使您决定必须拥有(大)Inventory aggregate,那么是的,它将加载自己生成的所有先前事件。然后InventoryItem将是一个嵌套的实体,它不能自己发出事件。

      

    这似乎可以轻松实施域规则,但我看到了一个主要的缺陷;将这些事件应用于聚合根时,您必须首先重建该InventoryItem集合。即使使用快照,对于大量项目而言效果似乎也非常低效。

    是的,的确如此。最简单的事情是我们所有人都有一个单一的聚合,只有一个实例。那么一致性将是最强的。但这并不高效,因此您需要更好地考虑真正的业务需求。

      

    另一种方法是每个InventoryItem有一个流跟踪所有仅与item有关的事件。每个流都使用该项的ID命名。这似乎是更简单的路线,但现在您将如何强制执行域规则,例如确保产品代码是唯一的,或者您不是将多个项目放在同一位置?

    还有另一种可能性。您应该将产品代码的分配建模为业务流程。为此,您可以使用Saga / Process管理器来协调整个过程。此Saga可以使用在产品代码列中添加唯一索引的集合,以确保只有一个产品使用给定的产品代码。

    您可以设计Saga以允许将已经采用的代码分配给产品并在以后进行补偿或首先拒绝无效分配。

      

    看起来你现在必须引入一个Read模型,但不是保持命令和查询分离的全部意义吗?这只是感觉不对。

    Saga确实使用了一个由域事件维护的私有状态,处于最终的一致状态,就像Read模型一样,但这对我来说并没有错。它可以使用它需要的任何东西,以便(最终)将系统作为一个洞带到一致的状态。它补充了聚合,其目的是不允许系统的构建块进入无效状态。

答案 1 :(得分:1)

在典型的事件存储中,每个事件流都是一个独立的事务边界。每次更改模型时,都会锁定流,附加新事件并释放锁定。 (在使用乐观并发的设计中,边界是相同的,但"锁定"机制略有不同)。

您几乎肯定希望确保将任何聚合包含在单个流中 - 在两个流之间共享聚合类似于在两个数据库之间共享聚合。

单个流可以专用于单个聚合,聚合集合,甚至整个模型。作为同一个流的一部分的聚合可以在同一个事务中更改 - huzzah! - 以从流中加载聚合时的一些争用和一些额外工作为代价。

最常讨论的设计将每个逻辑流分配给单个聚合。

  

这似乎可以轻松实施域规则,但我看到了一个主要的缺陷;将这些事件应用于聚合根时,您必须首先重建该InventoryItem集合。即使使用快照,对于大量项目而言效果似乎也非常低效。

有几种可能性;在某些模型中,特别是那些具有强时间成分的模型,对某些实体进行建模是有意义的。作为聚合的时间序列。例如,在计划系统中,而不是Bobs Calendar,而不是Bobs March CalendarBobs April Calendar等等。将生命周期缩小为较小的分期可以控制事件的数量。

另一种可能性是快照,还有一个额外的技巧:每个快照都使用元数据进行注释,这些元数据描述了快照在流中的位置,并且您只需从该点向前读取流。

当然,这取决于是否支持随机访问的事件流的实现,或者允许您在先出后读取的流的实现。

请注意,这些都是性能优化,而first rule of optimization是......不要。