事件源系统中的流聚合关系

Stream aggregate relationship in an event sourced system

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

根据我在 Internet 上收集到的信息,它可以被简洁地描述 "one stream per aggregate." 所以我会有一个 Inventory 聚合,一个包含 ItemAdded、ItemPulled、ItemRestocked 等事件的单个流,每个事件都包含序列化数据项目 ID、数量更改、位置等。聚合根将包含 InventoryItem 对象的集合(每个对象都有各自的数量、产品代码、位置等)。这似乎允许轻松执行域规则,但我看到一个主要缺陷;将这些事件应用于聚合根时,您必须首先重建 InventoryItem 的集合。即使使用快照,对于大量项目来说,这似乎也非常低效。

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

所以我的问题是 'which is correct?' 部分两者都有?两者都不?像大多数事情一样,我学得越多,我就知道我不知道的越多...

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

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

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

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

That seems like it would allow for easily enforcing domain rules, but I see one major flaw to this; when applying those events to the aggregate root, you would have to first rebuild that collection of InventoryItem. Even with snapshotting, that seems be very inefficient with a large number of items.

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

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

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

请记住,这两项都是真正的性能优化,而 first rule of optimization 是……不是。

So I'm trying to figure out the structure behind general use cases of a CQRS+ES architecture and one of the problems I'm having is how aggregates are represented in the event store

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

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

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

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

同时,关于聚合有一个 DDD 规则:每笔交易不要改变超过一个聚合。这个规则可以帮助你设计一个可扩展的系统。不需要就打断它。

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

So I would have an Inventory aggregate

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

That seems like it would allow for easily enforcing domain rules, but I see one major flaw to this; when applying those events to the aggregate root, you would have to first rebuild that collection of InventoryItem. Even with snapshotting, that seems be very inefficient with a large number of items.

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

Another method would be to have one stream per InventoryItem tracking all events pertaining to only item. Each stream is named with the ID of that item. That seems like the simpler route, but now how would you enforce domain rules like ensuring product codes are unique or you're not putting multiple items into the same location?

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

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

It seems like you would now have to bring in a Read model, but isn't the whole point to keep commands and query's seperate? It just feels wrong.

Saga 确实使用了从域事件维护的最终一致状态的私有状态,就像 Read-model 一样,但这对我来说并没有错。它可以使用任何它需要的东西来使(最终)系统作为一个洞达到一致的状态。它补充了聚合,其目的是不允许系统的building-blocks进入无效状态。