场景:
发票的收件人必须是有效的联系人。
在语音命令中创建:
{
"content": "my invoice content",
"recipient": "42"
}
我现在读了很多次,写端(=命令处理程序)不应该调用读端。
考虑到这一点,发票微服务必须侦听所有ContactCreated
和ContactDeled
事件,以便知道给定的收件人ID是否有效。
然后,我将在发票微服务中拥有数千个联系人,即使我知道他们中只有少数人会收到发票。
是否有任何最佳实践来处理这些场景?
发票的收件人必须是有效的联系人。
因此,您需要注意的第一件事是——如果两个实体是不同聚合的一部分,您不能真正实现“仅当该实体满足规范时才对该实体应用更改”,因为该实体可能会在您评估规范和执行写入之间发生变化。
换句话说,您只能跨聚合边界获得最终一致性。
聚合是它自己状态的权威,但其他一切(例如,命令消息的内容),它几乎必须接受一些外部权威已经检查了数据。
这里有几种方法可以选择
1)可以盲目接受命令中指定的收件人有效。
2)您可以尝试在从不受信任的来源接收到它并将其提交到域模型之间从某些外部机构(又名:其他聚合的读取模型)验证收件人的有效性。
3)您可以盲目地接受所描述的命令,但在确认收件人的有效性之前,将发票视为临时的。这意味着在发票上运行第二个命令来证明收件人。
注意-从模型的角度来看,这些不同的命令是等价的,但在应用层它们不需要-您可以将对命令的访问限制为受信任的来源(不要将其作为公共api的一部分,需要仅对受信任的来源可用的授权等)。
方法#3是最微服务的,因为这两个命令可以及时分离——您可以在CreateInVoice命令到达后立即接受它,并异步验证收件人。
你会把方法4)放在哪里,发票微服务有自己的联系人存储,每当有联系人创建或联系人删除事件时都会更新?然后这两个实体都是同一服务和边界的一部分。现在应该可以使事情保持一致,对吧?
不。您已经将两个实体作为同一服务的一部分,但问题从来都不是它们位于不同的服务中,而是它们位于不同的聚合中——这意味着我们可以同时更改实体状态,这意味着我们不能确保它们立即同步。
如果你想立即保持一致性,你需要一个以不同方式划分边界的模型。
例如,如果发票实体被建模为联系人聚合的一部分,那么聚合可以确保新发票需要有效收件人的不变性——域模型使用内存中的状态副本来确认我们加载时收件人是有效的,并且写入记录簿验证记录簿自加载发生以来没有更改。
聚合状态的写入是记录簿中的比较和交换;如果某个并发进程使收件人无效,CAS操作将失败。
当然,权衡的是,对联系人聚合的任何更改也会导致发票失败;与同一收件人同时编辑不同的发票是不可能的。
聚合是全有或全无;它们是不可分离的。
现在,一种可能是您的发票聚合有一部分必须与收件人立即一致,另一部分最终一致甚至不一致是可以接受的。在这种情况下,您的目标是重构模型。
发票的收件人必须是有效的联系人。
这是一个业务规则。应该问的问题是,这个业务规则对我的应用程序意味着什么?谁应该负责实现这个规则,还是可以分担责任?
一种可能性是,是的,业务规则是关于发票的,因此发票服务应该负责实施它。
然而,业务规则实际上是关于发票的创建。奇怪的是,您的架构中发票创建的所有者不是发票服务。这样做的原因是命令的名称是CreateInvoiceCommand
。
让我们考虑一下——发票服务永远不会只是自己创建发票。它只是提供了能力。在这种架构中,发票创建的实际所有者是命令的发送者。
使用这种推理方式,如果业务规则说不能针对无效的收件人创建发票,那么命令发送者有责任确保实现此业务规则。
如果发票服务订阅了事件,而不是接收命令,这将是一个非常不同的场景。例如,一个名为WidgetSold
的事件。在这种情况下,发票创建的所有者显然是发票服务,因此业务规则将在那里实现。
如果用户单击为联系人42创建发票按钮,则用户有责任注意联系人42是否存在
是的,这是正确的。用户的意图是创建发票。因此,此时应该强制执行围绕发票创建的业务规则。这是如何发生的(或者是否发生)是一个不同的问题。
但是如果用户不关心呢?然后它会创建一个收件人ID无效的发票。
同样正确。正如你所说,这种方法有副作用,其中之一是你最终会在整个系统中得到不一致的数据。这是SOA的现实之一。
这不是在某种程度上类似于这样吗:发票有一个CurrencyCode属性,它是一个字符串。
我不知道我是否同意。问这是一个有效的ISO货币吗?不同于问实体42是否根据另一个系统有效?。我会这么认为。
它是否有点相同,因为给定的收件人不为空,并且根据我的联系人数据库有效?
我同意,实际上,你可以在服务中实现这种验证。我只是说,我认为这不是合适的地方。如果你想这样做,你要么调用另一个服务,要么在本地存储所有联系人,就像你最初提出的问题一样。我认为在服务之外做更简单。
我认为答案取决于您希望系统具有多大的弹性,即如何处理联系人微服务
关闭(没有响应或非常慢)的情况。
1.你想变得非常有韧性
如果联系人微服务
关闭,您希望能够为一些(可能是大多数)联系人发出发票。在这种情况下,您可以监听ContactCreated
和ContactDeled
并维护一个(最终一致的)有效联系人的本地列表;他们应该在这个有限的上下文中根据普遍存在的语言命名,例如Payers
(或类似的东西)。然后,在应用程序层,当构建CreateInvoiceCommand
时,您检查Payer
是否有效并创建命令。
2.你不需要有韧性
如果联系人微服务
关闭,您将拒绝生成发票。在这种情况下,在构建命令时,您将向发票微服务
APIendpoint发出请求,并验证付款人
是否有效。
在任何情况下,您都可以在发送命令之前检查联系人的有效性。