事件采购中的流版本

时间:2016-07-25 17:08:19

标签: domain-driven-design event-sourcing

事件采购中,存储已为一个聚合实例发生的所有单个域事件,称为事件流< / em>的。与事件流一起,您还可以存储流版本

版本应该与每个域事件相关联,还是应该与事务性更改(也称为命令)相关?

示例:

我们目前的事件存储状态是:

aggregate_id | version | event
-------------|---------|------
1            | 1       | E1
1            | 2       | E2

在聚合1中执行新命令。该命令产生两个新事件E3和E4。

方法1:

aggregate_id | version | event
-------------|---------|------
1            | 1       | E1
1            | 2       | E2
1            | 3       | E3
1            | 4       | E4

使用这种方法,可以使用唯一索引通过存储机制完成乐观并发,但重放事件直到版本3可能使聚合/系统处于不一致状态。

方法2:

aggregate_id | version | event
-------------|---------|-----
1            | 1       | E1
1            | 2       | E2
1            | 3       | E3
1            | 3       | E4

重播事件直到版本3使聚合/系统处于一致状态。

谢谢!

4 个答案:

答案 0 :(得分:2)

简短回答:#1。

事件E3和E4的写入应该是同一事务的一部分。

请注意,在您关注的情况下,这两种方法并没有真正区别。如果您在第一种情况下的阅读可能会错过E4,那么您可以在第二种情况下阅读。在用于加载聚合以进行写入的用例中;加载前三个事件将告诉您下一个版本应为#4。

在方法#1的情况下,尝试编写版本4会产生唯一的约束冲突;命令处理程序将无法判断问题是否是数据的错误加载,或者只是一个乐观的并发失败,但在任何一种情况下结果都没有写入,并且记录簿仍处于一致状态。

在方法#2的情况下,尝试编写版本4不会与任何内容冲突。写入成功,现在你的E5与E4不一致。 Bleah。

有关事件存储模式的引用,您可以考虑查看:

我的首选架构,假设您被迫自己滚动,将流与事件分开。

stream_id    | sequence | event_id
-------------|----------|------
1            | 1        | E1
1            | 2        | E2

流为您提供了一个过滤器(流ID)来标识您想要的事件,以及一个顺序(序列),以确保您读取的事件与您编写的事件的顺序相同。但除此之外,它是一种人为的东西,是我们选择聚合边界的方式的副作用。所以它的作用应该非常有限。

实际的事件数据,它存在于其他地方。

event_id | data | meta_data | ...
--------------------------------------
E1       | ...  | ... | ...
E2       | ...  | ... | ...

如果您需要能够识别与特定命令相关联的事件,那么这是事件元数据的一部分,而不是流历史的一部分(请参阅:correlationId,causationId)。

答案 1 :(得分:2)

没有什么能阻止您引入commit_sequence以及version

例如,在NEventStore中,您可以看到提交的StreamRevision(版本 - 每个事件都在增加)和CommitSequence

答案 2 :(得分:0)

方法1是我使用过的并且看到其他人使用 - 只是事件的递增数字,通常称为EventNumber

乐观并发部分只是在加载聚合时,您知道最新事件是什么。然后,您处理该命令并保存任何结果事件 - 如果您看到任何超出您加载的数字的任何内容,则意味着您已经过时并且可以采取相应行动,否则您可以保存事件。

答案 3 :(得分:0)

在带有事件源的域驱动设计中,聚合表示一致性边界,并且其不变量必须在每个命令的开头和结尾处为 (或函数调用) )。你可以在成员函数的中间违反一个不变量,只要它在结尾处没有被违反。

你在帖子中指出的是非常有见地的。也就是说,如果聚合上的单个命令(或对成员函数的调用)产生多个事件,那么只有存储其中一些事件可能会导致在另一个进程从磁盘重新加载聚合时违反您的不变量。将SQL数据库用作事件存储时,有许多相关方案可能会产生此问题。

避免这种情况的第一种(也是最简单的)方法是将所有事件INSERT语句包装到事务中,以便所有事件都是持久的,或者都不是(例如,由于并发)。这样,就可以保持不变量的“磁盘上”表示形式。您还必须确保您的事务隔离级别不是“READ UNCOMMITTED”(以便其他进程看不到您提交的一半)。您还必须确保数据库不会在进程之间“交错”事件序列号。例如,数据库为进程A中的事件分配序列号“1”,为进程B中的事件分配序列号“2”,然后再为进程A中的第二事件分配序列号“3”。所有事件都可以提交到数据库,因为并发约束(聚合ID +事件序列号)没有冲突,但事件序列由两个进程写入,因此您的不变量可能仍然被违反。

第二个选项是将所有事件包装到一个使用单个INSERT语句持久化的数组中。这实际上会导致每个提交都有一个版本号,而不是每个事件的版本号。对我来说,这更符合逻辑,但它要求您在将事件数组发送给各种事件处理程序和流程管理器之前,先设置一个“解除”事件数组的程序。我个人在一个项目中使用第二种机制,该项目以磁盘上的原始二进制格式存储事件。事件本身仅包含聚合更改状态所需的最少量信息 - 事件不包括聚合标识符。另一方面,提交确实包含聚合标识符,提交序列号和各种其他元数据。这基本上将聚合之间的功能分离为未提交事件的处理程序已提交事件的事件处理程序。这种区别也是有道理的,因为如果一个事件是一个“事实” - 发生了什么事情 - 那么聚合所做的事情与聚合所做的事实是否实际保存到磁盘之间存在差异。

从理论上讲,你的问题的一个很好的例子是链表 - 只考虑内存中的表示:磁盘上没有持久性。您在向量或数组上使用链表的原因之一是它允许有效插入节点(嗯,比数组更有效)。插入操作通常要求将当前节点的“下一个”指针设置为新节点的存储器地址,并将新节点的“下一个”指针设置为当前节点的前一个“下一个”指针。如果另一个进程在第一个操作完成后但在第二个操作完成之前在内存中读取相同的链表,则不会看到链表中的所有节点。如果每个“操作”都像一个“事件”,那么只看到第一个事件会导致读者看到一个损坏的链表。