CQRS/ES 架构上的补偿事件

Compensating Events on CQRS/ES Architecture

所以,我正在做一个 CQRS/ES 项目,在这个项目中我们对如何处理在其他架构中很容易处理的小问题有一些疑问

我的场景如下:

我有一个客户 CRUD REST API 并且每个客户都有唯一的文档(编号),所以当我注册一个新客户时,我必须验证是否有另一个客户拥有该文档以避免重复,但是当涉及到我们具有最终一致性的 CQRS/ES 架构时,我发现这种验证可能很难解决。

需要注意的是,我的问题不是跨微服务,而是在同一个微服务的命令应用和查询应用之间。

我们也在使用 eventstore

我目前的解决方案:

所以我今天要做的是,在我的命令应用程序中,在保存 CustomerCreated 事件之前,我询问查询应用程序(使用 PostgreSQL)是否有该文档的客户,如果没有,我允许该事件继续。但这并不能保证 100%,对吗?因为我的查询可以去同步化,所以我不能 100% 信任它。那是我的第二次验证开始的时候,当我的查询应用程序正在处理事件并将它们保存到我的 PostgreSQL 时,我再次检查是否有该文档的客户,如果有,我拒绝该事件并向 undo/cancel/inactivate 具有重复文档的客户,因此在 eventstore 上完成该客户流。

虽然这可行,但这里有两件事困扰我,第一件事是我的命令应用程序依赖于查询应用程序,所以如果我的查询应用程序关闭,我的命令会受到影响(今天我只是 return false 在我的验证中如果查询已关闭但仍然......)第二件事是,query/read 模型真的应该能够发出事件吗?如果是这样,正确的做法是什么?该命令应该为此提供某种 API 吗?还是查询应该使用一些公共共享库将事件直接发送到事件存储?如果我有多个 view/read 呢?我应该选择哪一个来处理这个问题?

真的希望有人能阐明这些问题并帮助我解决这些问题。

作为参考,您可能想要查看 Greg Young 写的关于 Set Validation 的内容。

I ask the query application (using PostgreSQL) if there is a customer with that document, and if not, I allow the event to go on. But that doesn't guarantee 100%, right?

完全正确 - 您的读取模型是陈旧的副本,可能没有写入模型收集的所有信息。

That's when my second validation kicks in, when my query application is processing the events and saving them to my PostgreSQL, I check again if there is a customer with that document and if there is, I reject that event and emit a compensating event to undo/cancel/inactivate the customer with the duplicated document, therefore finishing that customer stream on eventstore.

这种拼写与通常的设计不太相符。更常见的实现是,如果我们在读取数据时检测到问题,我们会向写入模型发送一条命令消息,告诉它解决问题。

这通常被称为流程管理器,但您可以将其视为 human supervisor of the system 的自动化。从概念上讲,流程管理器是要发送到命令模型的消息的事件源集合。

您可能还想考虑是否正确地为域建模。如果文档应该是唯一的,那么命令模型可能应该使用文档编号作为记录簿中的关键字,而不是使用客户。或者文档 ID 应该是客户数据的函数,而不是任意输入。

as far as I know, eventstore doesn't have transactions across different streams

对 - 一般来说,您真正需要考虑的事情之一是您的流边界所在的位置。如果集合验证具有重要的 业务价值 ,那么您确实需要考虑将整个集合放入单个流中(或者通过找到一种不使用集合来约束唯一性的方法)。

How should I send a command message to the write model? via API? via a message broker like Kafka?

那是管道;只要您确定该命令在其自己的 transaction/unit 工作范围内运行,您如何操作并不重要。

So what I do today is, in my command application, before saving the CustomerCreated event, I ask the query application (using PostgreSQL) if there is a customer with that document, and if not, I allow the event to go on. But that doesn't guarantee 100%, right? Because my query can be desynchronized, so I cannot trust it 100%.

不,您不能安全地依赖最终一致的查询端来防止系统进入无效状态。

你有两个选择:

  1. 您允许系统进入一个临时的、挂起的状态,然后,最终,您将使它进入一个有效的永久状态;为此,您可以允许命令通过,产生 CustomerRegistered 事件并使用 Saga/Process 管理器验证 uniquely-indexed-by-document-collection 并发出补偿命令(不是事件!),即 UnregisterCustomer.

  2. 不是发送命令,而是创建并启动一个 Saga/Process,它在 uniquely-indexed-by-document-collection 中预分配文档,如果成功,则发送 RegisterCustomer 命令。您可以将 Saga 建模为一个实体。

因此,在这两种解决方案中,您都使用 Saga/Process 管理器。为了使系统具有弹性,您应该确保 RegisterCustomer 命令是幂等的(因此如果 Saga fails/is 重新启动,您可以重新发送它)

您遇到了一个相当普遍的问题。我认为 VoicOfUnreason 的另一个答案值得一读。我只是想让您了解更多选项。

  1. 我过去使用的一种简单方法是创建查找 table。您的命令尝试在唯一约束 table 中注册密钥。如果它可以保留密钥,则命令可以继续。

  2. 根据数据的性质和域,您可以让这个 'problem' 发生并引发其他事件来标记它。如果它对 business/the 应用程序的工作方式很重要,那么您可以手动或通过补偿命令同时处理它。如果是后者,那么使用流程管理器就有意义了。

  3. 在某些(罕见的)情况下,speed/capacity 不是什么大问题,那么您可以考虑 old-fashioned 锁定和事务。诚然,这些更适合 CRUD 风格的实现,但它们可以在 CQRS/ES 中使用。

我的博客中有更多详细信息 post:How to Handle Set Based Consistency Validation in CQRS

希望对您有所帮助。