CQRS 聚合和投影一致性

问题描述

Aggregate 可以使用 View 这个事实在 Vaughn Vernon 的书中有描述:

此类读取模型投影经常用于向各种客户端(例如桌面和 Web 用户界面)公开信息,但它们对于在限界上下文及其聚合之间共享信息也非常有用。考虑发票汇总需要一些客户信息(例如姓名、帐单地址和税号)以计算和准备正确发票的场景。我们可以通过 CustomerBillingProjection 以易于使用的形式捕获此信息,它将创建和维护 CustomerBilling-View 的独占实例。此读取模型可通过名为 IProvideCustomerBillinginformation 的域服务提供给 Invoice Aggregate。在幕后,此域服务只是查询文档存储以获取 CustomerBillingView 的适当实例

让我们想象一下,我们的应用程序应该允许创建许多用户,但具有唯一的名称。命令/事件流:

  • createuser{Alice} 命令已发送
  • UserAggregate 检查 UsersListView,因为没有名为 Alice 的用户,aggregate 决定创建用户并发布事件。
  • UserCreated{Alice} 事件已发布 // 由 UserAggregate
  • UsersListProjection 处理 UserCreated{Alice} // 为简单起见,我们认为 UsersListProjection 只是在收到 UserCreated event 时累积用户名
  • createuser{Bob} 命令已发送
  • UserAggregate 检查 UsersListView,因为没有名为 Bob 的用户,aggregate 决定创建用户并发布事件。
  • UserCreated{Bob} 事件已发布 // 由 UserAggregate
  • createuser{Bob} 命令已发送
  • UserAggregate 检查 UsersListView,因为没有名为 Bob 的用户,aggregate 决定创建用户并发布事件。
  • UsersListProjection 已处理 UserCreated{Bob} .
  • UsersListProjection 已处理 UserCreated{Bob} .

问题是 - UsersListProjection 没有时间处理事件并且包含不相关的数据,聚合使用了这些不相关的数据。结果 - 创建了 2 个同名用户

如何避免这种情况? 如何使汇总和预测保持一致?

解决方法

如何使汇总和预测保持一致?

在一般情况下,我们不会。预测与过去某个时间的总体一致,但不一定包含所有最新更新。这就是问题的一部分:我们放弃“即时一致性”以换取其他(更高的杠杆)好处。

您提到的重复通常以不同的方式解决:通过使用条件写入记录簿。

在您的示例中,我们通常会设计系统,以便第二次尝试将 Bob 写入我们的数据存储会因冲突而失败。此外,我们通过确保对数据存储的写入发生在任何事件可见之前,从而防止重复传播。

这实际上给了我们一个“先写者胜”的写策略。输掉数据竞争的写入器必须重试/失败/等。

(通常,这取决于两个尝试创建 Bob 的想法,使用相同的锁将该信息写入相同的位置。)

降低冲突可能性的一种常见设计是不使用聚合本身的“读取模型”,而是使用其在数据存储中自己的数据。这不一定会消除所有数据竞争,但您可以减少窗口的宽度。

最后,我们回到 Memories,Guesses and Apologies

,

请务必记住,在 CQRS 中,每个写入模型也是用于验证命令所需的读取的读取模型。这些读物是:

  • 检查是否存在具有特定 ID 的聚合
  • 加载整个聚合的最新版本

通常,CQRS/ES 实现将为您提供读取模型。如何实施的细节将取决于实施。

这些是命令处理程序需要执行的唯一读取,如果查询的回答不超过这些读取,则查询可以表示为一个命令(例如 GetUserByName{Alice}),当处理该命令时不发出事件。这种只读命令的好处是它们可以高度一致,因为它们仅限于单个聚合。当然,并非所有查询都可以用这种方式表达,如果查询可以容忍最终的一致性,则可能不值得为强一致性支付协调税,您通常通过将其设为只读命令来支付这种费用。 (仅限于单个聚合的命令处理通常是强一致性的,但在某些情况下,例如,当事件形成 CRDT 并且聚合可以存在于多个数据中心时,即使这种一致性也会松散。

考虑到这一点:

  • 收到CreateUser{Alice}
  • 用户 Alice 不存在
  • 坚持UserCreated{Alice}
  • CreateUser{Alice} 已确认(例如 HTTP 200、对 *MQ 的确认、Kafka 偏移量提交)
  • UserListProjection 更新自 UserCreated{Alice}
  • 收到CreateUser{Bob}
  • 用户 Bob 不存在
  • 坚持UserCreated{Bob}
  • CreateUser{Bob} 已确认
  • 收到CreateUser{Bob}
  • 用户 Bob 已存在
  • 现有用户的命令处理程序拒绝该命令并且不保留任何事件(它可能会记录尝试创建重复用户)
  • CreateUser{Bob} 确认失败(例如 HTTP 401、确认到 *MQ、Kafka 偏移提交)
  • UserListProjection 更新自 UserCreated{Bob}

请注意,虽然 UserListProjection 可以回答“此用户是否存在?”的问题,但写入方也可以(并且更一致地)回答该问题的事实本身并不能做出这种预测多余。 UserListProjection 还可以回答诸如“谁是所有用户?”之类的问题。或者“哪些用户的名字中有两个连续的元音?”写方无法回答。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...