【问题标题】:CQRS + Microservices: How to handle relations / validation?CQRS + 微服务:如何处理关系/验证?
【发布时间】:2017-07-20 14:27:50
【问题描述】:

场景:

  • 我有 2 个微服务(它们都在内部使用 CQRS + 事件溯源)
  • 微服务 1 管理联系人(= 聚合根)
  • 微服务 2 管理发票(= 聚合根)

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

CreateInvoiceCommand:

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

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


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

然后我会在 Invoices 微服务中拥有数千个联系人,即使我知道其中只有少数人会收到发票。


是否有处理这些场景的最佳实践?

【问题讨论】:

    标签: validation relationship one-to-many microservices cqrs


    【解决方案1】:

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

    所以你需要注意的第一件事 - 如果两个实体是不同聚合的一部分,你不能真正实现“仅当 那个 实体满足规范时才对这个实体应用更改",因为那个实体可能会在您评估规范和执行写入之间发生变化。

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

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

    您可以在这里采取几种方法

    1) 你可以盲目接受命令中指定的接收者是有效的。

    2) 您可以尝试在从不受信任的来源接收它并将其提交到域之间从某个外部权威机构(又名:某个其他聚合的读取模型)验证收件人的有效性型号。

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

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

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

    您会将方法 4) 放在哪里,发票微服务有自己的联系人存储,只要有 ContactCreated 或 ContactDeleted 事件就会更新?那么这两个实体都是相同服务和边界的一部分。现在应该可以让事情保持一致了,对吧?

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

    如果您想要立即保持一致性,您需要一个以不同方式划分界限的模型。

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

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

    当然,权衡是任何对联系人聚合的更改也会导致发票失败;对同一个收件人同时编辑不同的发票会消失。

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

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

    【讨论】:

    • 谢谢!您会将方法 4) 放在哪里,发票微服务有它自己的联系人存储,只要有ContactCreatedContactDeleted 事件就会更新?那么这两个实体都是相同服务和边界的一部分。现在应该可以使事情保持一致,对吧? (除非命令包含新创建的联系人,但 Invoices 微服务尚未处理 ContactCreated 事件 - 这也使事情最终保持一致?!)
    【解决方案2】:

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

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

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

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

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

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

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

    如果用户点击为联系人 42 创建发票按钮,则 用户有责任注意联系人 42 的存在

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

    但如果用户不关心怎么办?然后它会创建一个发票 收件人 ID 无效。

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

    这不是有点类似:发票有一个货币代码 属性,它是一个字符串。

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

    是不是有点像给定的收件人不是空的并且是有效的 根据我的联系人数据库?

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

    【讨论】:

    • 谢谢。这听起来很有趣也很合理。让我总结一下,看看我是否真的明白你的意思:如果用户单击contact 42 创建发票 按钮,则用户有责任注意contact 42 的存在。看起来很简单。 但是如果用户不关心怎么办?然后它会创建一个带有无效收件人 ID 的发票。意味着:我的数据库中有垃圾。不好吗?是的,但仅限于可用磁盘空间。但它不会给应用程序本身带来任何麻烦,因为它只是一个孤立的数据。
    • 好的。我仍然不明白的(希望)唯一的事情是:为什么 valid contact 是一个业务规则并且不应该是 Invoice 的 CommandHandler 的一部分?这不是有点类似:发票有一个currencyCode 属性,它是一个字符串。在我看来,CommandHandler 现在将验证提供的currencyCode 不为空,并且根据 ISO 4217 有效。它与 给定收件人不为空并且根据我的联系人数据库有效?也许我完全错了。请赐教。 :)
    【解决方案3】:

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

    1.你想要非常有弹性

    如果Contacts Microservice 已关闭,您希望能够为部分(可能是大部分)联系人开具发票。在这种情况下,您收听ContactCreatedContactDeleted 并维护一个(最终一致的)有效 联系人的本地列表;在这个有界上下文中,它们应该根据通用语言命名,例如Payers(或类似的东西)。然后,在应用层中,在构建CreateInvoiceCommand 时,检查Payer 是否有效并创建命令。

    2。你不需要有弹性

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

    在任何情况下,您都要在发送命令之前检查联系人的有效性。

    【讨论】:

    • 谢谢。您的第一点听起来像允许从写入端调用读取端。但我到处读到应该避免它。还是他们只是认为您不应该称自己为阅读方? (即 InvoiceCommandHandler 不应该执行 InvoiceQuery)? ...为什么要检查有效性之前命令被调度?我认为验证应该是命令处理程序的一部分?
    • 您之前检查是因为聚合无法查询外部服务。聚合必须仅基于以前的事件和当前命令强制执行不变量。它应该是纯净的。
    • 我知道聚合不能做到这一点。但据我了解,流程是这样的:1.Command -> CommandHandler、2.CommandHandler 验证 Command、3.CommandHandler 修改 Aggregate(通过调用 Aggregate 上的方法)、4.Aggregate 发布事件等。 ...这就是为什么我认为命令处理程序应该检查给定的联系人 ID 是否有效。之后它将开始聚合修改。所以聚合不会做任何查询。命令处理程序可以。 ...但也许我很困惑,因为我最近几天阅读了数百万篇 CQRS 文章。
    • @BenjaminM 至少有两种样式:[1]。command 是通过Application service 直接发送到Aggregate,所以command handler 是Aggregate 上的一个方法(我喜欢的风格,见cqrs.nu/tutorial)和[2]。 command handlerApplication service,它调用聚合上的方法。在这两种情况下,我的观点是验证是在Application service 中完成的,因此Aggregate 保持纯净。
    • 啊……好吧。我只知道 Style [2]。谢谢!对于样式 [2],您说 “命令处理程序是应用程序服务”“验证在应用程序服务中完成”。如果我使用样式 [2],我是否可以在命令处理程序中进行验证?我很清楚不应该在聚合中进行验证。
    猜你喜欢
    • 2018-11-28
    • 2017-12-05
    • 2018-02-21
    • 2019-12-03
    • 2018-06-24
    • 2017-11-09
    • 2018-09-24
    • 2022-01-02
    • 2018-08-12
    相关资源
    最近更新 更多