CQRS + 微服务:如何处理关系/验证?

CQRS + Microservices: How to handle relations / validation?

场景:

发票的收件人必须是有效的联系人。

创建发票命令:

{
  "content": "my invoice content",
  "recipient": "42"
}

我现在读了很多次,写入端(= 命令处理程序)不应该调用读取端。


考虑到这一点,发票微服务必须侦听所有 ContactCreatedContactDeleted 事件才能知道给定的收件人 ID 是否有效。

然后我在发票微服务中有成千上万的联系人,即使我知道他们中只有少数人会收到发票。


是否有处理这些情况的最佳实践?

The recipient of an invoice must be a valid contact.

这是商业规则。应该问的问题是,这条业务规则对我的应用意味着什么?谁应该负责执行这条规则,或者责任可以分担吗?

一种可能性是,是的,业务规则是关于发票的,因此发票服务应该负责实施它。

但是,业务规则实际上是关于发票的创建。奇怪的是,在您的体系结构中创建发票的所有者不是发票服务。原因是命令的名称是CreateInvoiceCommand.

让我们考虑一下 - 发票服务绝不会自行创建发票。它只是提供能力。在此架构中,发票创建的实际所有者是命令的发送者。

使用这种推理,如果业务规则规定不能针对无效收件人创建发票,那么命令发送者有责任确保实施此业务规则。

如果发票服务订阅事件而不是接收命令,这将是一个非常不同的场景。例如,名为 WidgetSold 的事件。在这种情况下,发票创建的所有者显然是 Invoicing 服务,因此业务规则将在那里实施。

If the user clicks the create invoice for contact 42 button, it's the user's responsibility to take care that contact 42 exists

是的,没错。用户的意图是创建发票。因此,此时应强制执行有关发票创建的业务规则。这是如何发生的(或者这是否发生了)是另一个问题。

But what if the user doesn't care? Then it would create an invoice with an invalid recipient id.

也正确。正如您所说,这种方法有副作用,其中之一就是您最终可能会在整个系统中得到不一致的数据。这是 SOA 的现实之一。

Isn't this somehow similar to this: The Invoice has a currencyCode property, it's a String.

不知道同意不同意。询问这是一种有效的 ISO 货币吗? 与询问 根据另一个系统实体 42 是否有效? 不同。我会这么认为。

Isn't it kinda the same as given recipient is not null and is valid according to my Contacts Database?

我同意,实际上,您可以在服务中实施此验证。我只是说我不认为这是合适的地方。如果您想这样做,您将不得不调用另一项服务或将所有联系人存储在本地,就像您最初提出问题一样。我认为在服务之外进行更简单。

我认为答案取决于您希望系统的弹性如何,即如何处理 Contacts Microservice 出现故障(无响应或非常慢)的情况。

1.你想变得很有韧性

如果 Contacts Microservice 出现故障,您希望能够为某些(也许是大多数)联系人开具发票。在这种情况下,您收听 ContactCreatedContactDeleted 并维护一个(最终一致的)本地 有效 联系人列表;在这个有界上下文中,它们应该根据无处不在的语言命名,比如 Payers (或类似的东西)。然后,在应用层中,在构建 CreateInvoiceCommand 时检查 Payer 是否有效并创建命令。

2。你不需要有韧性

如果Contacts Microservice出现故障,您拒绝生成发票。在这种情况下,在构建命令时,您向 Invoices Microservice API 端点发出请求并验证 Payer 是否有效。

无论如何,在发送命令之前检查联系人的有效性。

The recipient of an invoice must be a valid contact.

所以您首先需要注意的是 - 如果两个实体是不同聚合的一部分,您无法真正实现 "apply a change to this entity only if that entity satisfies a specification",因为 那个 实体在评估规范和执行写入之间可能会发生变化。

换句话说 - 您只能在聚合边界上获得最终一致性。

聚合是其自身状态的权威,但其他一切(例如,命令消息的内容),它几乎必须接受一些外部权威已经检查了数据。

您可以在此处采用几种方法

1) 可以盲目接受命令中指定的收件人有效

2) 您可以尝试在 从不受信任的来源接收 和将其提交给域模型。

3) 您可以按照描述盲目接受命令,但在确认收件人的有效性之前,将发票视为临时发票。这意味着在证明收件人的发票上有第二个命令 运行。

注意 - 从模型的角度来看,这些不同的命令是等效的,但在应用程序层它们不需要 - 您可以将对命令的访问限制为受信任的来源(不要使其成为 public api 的一部分,需要仅对受信任来源等可用的授权。

方法 #3 是最微服务的,因为这两个命令可以及时分开——您可以在 CreateInvoice 命令到达时立即接受它,并异步验证收件人。

Where would you put approach 4), where the Invoices Microservice has it's own Contacts Store which gets updated whenever there's a ContactCreated or ContactDeleted event? Then both entities are part of the same service and boundary. Now it should be possible to make things consistent, right?

没有。您已将这两个实体设为同一服务的一部分,但问题不在于它们位于不同的 服务 中,而是它们位于不同的 聚合 [=52] 中=] -- 这意味着我们可以同时更改实体状态,这意味着我们无法确保它们立即同步。

如果您想要即时的一致性,您需要一个能够以不同方式划定界限的模型。

例如,如果发票实体被建模为联系人聚合的一部分,则聚合可以确保新发票需要有效收件人这一不变性——域模型使用内存中的状态副本来确认收件人 在我们加载时是 有效的,写入记录簿验证记录簿自加载发生后没有更改。

聚合状态的写入是记录簿中的比较和交换;如果某些并发进程使接收者无效,CAS 操作将失败。

当然,需要权衡的是,任何 对联系人聚合的更改也会导致发票失败;同时编辑具有相同收件人的不同发票超出 window。

聚合是全有或全无;他们是不可分离的。

现在,一个可能是您的发票合计有一部分必须与收件人立即一致,而另一部分最终一致,甚至不一致是可以接受的。在这种情况下,您的目标是重构模型。