问题描述
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
还可以回答诸如“谁是所有用户?”之类的问题。或者“哪些用户的名字中有两个连续的元音?”写方无法回答。