如何在 CQRS / 事件溯源中以确定性方式重播?

How to replay in a deterministic way in CQRS / event-sourcing?

在基于 CQRS/ES 的系统中,您将事件存储在事件存储中。这些事件引用一个聚合,并且它们具有关于它们所属的聚合的顺序。此外,聚合是一致性/事务边界,这意味着任何事务保证仅在每个聚合级别上给出。

现在,假设我有一个读取模型,它使用来自 多个 聚合的事件(这非常好,AFAIK)。为了能够以确定的方式重放读取模型,事件需要某种全局排序,跨聚合——否则你不知道是在 B 的事件之前还是之后重播聚合 A 的事件,或者如何混合他们。

实现此目的的最简单解决方案是在事件上使用时间戳,但通常时间戳不够细粒度(或者,换句话说,并非所有数据库都是平等创建的)。另一种选择是使用全局序列,但这在性能方面很糟糕并且会阻碍缩放。

你是怎么解决这个问题的?或者我的基本假设是,读取模型的重播应该是确定性的,错了吗?

How do you solve this issue?

这是已知问题,当然,简单的时间戳、全局序列或事件朴素方法都无济于事。
使用带有弱时间戳的 vector clock 来枚举您的事件,并使用矢量光标来读取它们。这保证了一些稳定的确定性顺序来混合聚合之间的事件。即使每个线程都有时钟同步间隙,这也是数据库集群的常规用例,因为完美的时间戳同步是不可能的。
这也自动提供了以后从事件存储和事件总线无缝混合读取事件的可能性,并排除了不同聚合事件之间的任何数据库锁定。

算法草稿:
1) 确定数据库中同时发生的事务的实际数量,例如集群中的最大工人数。
由于每个事件仅在一个线程中的一个事务中写入,您可以将其唯一 ID 确定为元组 (thread number, thread counter),其中线程计数器是当前线程处理的事务量。
将事件弱时间戳计算为 MAX(thread timestamp, aggregate timestamp),其中聚合时间戳是当前聚合的最后一个事件的时间戳。

2) 准备矢量游标以通过线程号边界读取事件。按顺序从每个线程读取事件,直到时间戳间隙超过允许值。允许的弱时间戳差距是事件读取性能和保留本机事件顺序之间的权衡。
最小值是集群线程同步时间增量,因此事件以本地聚合混合顺序到达。最大值是无穷大,因此事件将被聚合吐出。当使用像 postgres 这样的 RDBMS 时,该值可以通过智能 SQL 查询自动确定。

您可以看到 saving events and loading events 的 PostgreSQL 数据库的引用实现。对于 4GB RAM RDS Postgres 集群,保存事件性能约为每秒 10000 个事件。

我看到了这些选项:

  • 全局序列

    • 如果你的数据库允许,可以使用timestamp+aggregateId+aggregateVersion作为索引。这通常在分布式数据库情况下效果不佳。

    • 在分布式数据库中可以使用vector clock获取全局序列而不用加锁

  • 每个读取模型中的事件序列。您可以按字面意思将所有事件存储在读取模型中,并在应用投影函数之前根据需要对它们进行排序。

  • 允许非确定性并处理它。例如,在您的示例中,如果 add_user 事件到达时没有组 - 只需为读取模型创建一个空组记录并添加一个用户。当 create_group 事件到达时 - 更新该组记录。 毕竟,您已经签入 UI and/or 命令处理程序 有这个aggregateId的群对吧?