【问题标题】:Stream aggregate relationship in an event sourced system事件源系统中的流聚合关系
【发布时间】:2018-03-14 21:51:57
【问题描述】:

所以我试图弄清楚 CQRS+ES 架构的一般用例背后的结构,我遇到的问题之一是聚合在事件存储中的表示方式。如果我们将事件分成流,流到底代表什么?在一个假设的库存管理系统的上下文中,该系统跟踪一组项目,每个项目都有一个 ID、产品代码和位置,我在可视化系统布局时遇到了麻烦。

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

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

所以我的问题是“哪个是正确的?”部分两者兼而有之?两者都不?像大多数事情一样,我学的越多,我不知道的就越多……

【问题讨论】:

  • 对我来说,如果您在一个聚合中遇到太多事件的问题,这表明您需要重新考虑您的界限。

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


【解决方案1】:

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

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

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

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

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

有几种可能性;在某些模型中,尤其是那些具有强时间分量的模型中,将某些“实体”建模为聚合的时间序列是有意义的。例如,在调度系统中,您可能有Bobs March CalendarBobs April Calendar 等等,而不是Bobs Calendar。将生命周期分成更小的部分可以控制事件计数。

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

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

请记住,这两个都是真正的性能优化,first rule of optimization 是......不要。

【讨论】:

  • 所以本质上这是一个错误的困境;这两种方法都是 valid 方法,因为您没有踩到任何事务边界。只是哪一个取决于特定的应用程序。
  • 我相信它们在一致性上是不同的。如果第一个加载整个集合,它可以在应用事件之前检测到集合中的重复数据,从而防止无效状态,但现在必须加载(例如)所有 100,000 个项目。另一个可以只允许添加,并建立一个读取模型。然后一个服务监听添加事件,然后根据新的读取模型检查它并异步应用补偿命令。权衡是可能的暂时无效状态。好消息是我认为我的域可以处理这个问题。还有更好的方法吗?
  • 流是事务边界。聚合是事务边界。好像这里有比赛,不是吗?
  • 不,因为我拒绝这个前提。聚合由一致性边界定义,这与事务边界不同。当两个概念对齐时很方便,但它们不是一回事。
【解决方案2】:

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

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

  1. 高效加载之前由聚合根实例(具有给定的指定 ID)发出的所有事件
  2. 必须按照发出的顺序检索这些事件
  3. 它不能允许同时为同一个聚合根实例附加事件
  4. 作为单个命令的结果发出的所有事件都必须以原子方式附加;这意味着他们应该全部成功或全部失败

第四点可以使用事务来实现,但这不是必需的。事实上,出于可伸缩性的原因,如果可以,那么您应该选择一种无需使用事务即可为您提供原子性的持久性。例如,您可以将事件存储在 MongoDB 文档中,因为 MongoDB 保证文档级别的原子性。

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

同时,有一条关于聚合的 DDD 规则:每笔交易不要变异多个聚合。这条规则可以帮助您设计一个可扩展的系统。如果您不需要,请打破它。

因此,所有这些要求的解决方案是称为 Event-stream 的东西,它包含 Aggregate 实例之前发出的所有事件。

所以我会有一个 Inventory 聚合

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

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

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

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

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

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

您现在似乎必须引入 Read 模型,但将命令和查询分开不就是重点吗?就是感觉不对。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-28
    • 1970-01-01
    • 2022-09-24
    • 2014-08-05
    • 2014-11-21
    • 1970-01-01
    • 2019-11-15
    相关资源
    最近更新 更多