前面的底线:您正在寻找Command Query Responsibility Segregation;它定义了一种架构模式,用于分解从查询数据到请求运行流程的职责。简短的回答是您不想在查询或进程中以阻塞方式混合两者。这个答案的其余部分将详细说明原因,以及您可以做您想做的事情的三种不同方式。
这个答案是我在微服务方面的经验的简短形式。我的诚意:我从头开始创建了微服务拓扑(并且知识几乎为零),正如他们所说,在下降的过程中触及了每个分支。
从零知识开始的一个好处是,我创建的第一个拓扑混合使用了服务内同步和阻塞 (HTTP) 通信(从持有它的服务检索操作所需的数据)和消息队列+ 运行操作的异步事件(用于命令)。
我将定义这两个术语:
命令:告诉服务做某事。例如,“运行 ETL 批处理作业”。你期望有一个输出;但这必然是一个您无法可靠等待的过程。命令有副作用。由于此操作,某些事情会发生变化(如果没有发生任何事情并且没有任何变化,那么您没有做任何事情)。
查询:向服务询问它持有的数据。由于给出了命令,这些数据可能已经存在,但请求数据不应该有副作用。由于收到了查询,因此不需要运行任何命令操作。
无论如何,回到拓扑。
级别 1:混合 HTTP 和事件
对于第一个拓扑,我们将同步查询与发出的异步事件混合在一起。这是……有问题的。
消息总线本质上是可观察的。RabbitMQ 中的一项设置,或者一个事件源,您可以观察系统中的所有事件。这有一些很好的副作用,因为当过程中发生某些事情时,您通常可以找出导致该状态的事件(如果您遵循事件驱动的范例 + 状态机)。
如果不检查网络流量或记录这些请求,HTTP 调用是不可观察的(这本身就有问题,所以我们将从正常操作中的“不可行”开始)。因此,如果您将基于消息的进程和 HTTP 调用混合在一起,您将有无法判断发生了什么的漏洞。由于网络错误,您的 HTTP 调用没有返回数据,您的服务因此没有继续该过程。您还需要为您的 HTTP 调用连接 Retry/Circuit Breaker 模式,以确保它们至少尝试几次,但是您必须区分“由于它已关闭而未启动”和“由于暂时繁忙而未启动” ”。
简而言之,将这两种方法混合用于命令驱动过程并不是很有弹性。
级别 2:事件定义数据的 RPC/内部请求/响应;查询是外部的
在此成熟度模型的第二步中,您将命令和查询分开。命令应该使用事件驱动系统,查询应该通过 HTTP 发生。如果您需要命令查询的结果,那么您发出一条消息并在您的消息总线上使用请求/响应模式。
这也有好处和问题。
利益方面,您的整个指挥部现在是可观察的,即使它通过多个服务跳跃。您还可以通过重新运行事件来重放系统中的进程,这对于跟踪问题很有用。
问题方面,现在您的一些事件看起来很像查询;现在,您正在为消息重新创建 HTTP 中可用的漂亮 HTTP 和 REST 语义;这不是非常有趣或有用。例如,404 告诉您 REST 中没有数据。对于基于消息的事件,您必须重新创建这些语义(有一个很好的 Youtube 会议上关于我找不到的主题的演讲,但是一个团队试图这样做很痛苦)。
但是,您的事件现在是异步且非阻塞的,并且每个服务都可以重构为响应给定事件的状态机。需要注意的是,这些事件应该包含操作所需的所有数据(这会导致消息在流程过程中不断增长)。
您的查询仍然可以使用 HTTP 进行外部通信;但对于内部命令/进程,您将使用消息总线。
我也不推荐这种方法(尽管它比第一种方法更上一层楼)。我不推荐它,因为您的事件开始出现杂质,并且在微服务系统中,整个系统的合同相同是很重要的。
第 3 级:数据的生产者将数据作为事件发出。消费者记录数据以供使用。
成熟度模型的第三步(当我离开项目时,我们正朝着那个范式前进)是为产生数据的服务提供在数据产生时发出事件的服务。然后,监听这些事件的服务会记下这些数据,这些服务将使用这些(可能是?)陈旧的数据来执行它们的操作。外部客户仍然使用 HTTP;但是在内部,当产生新数据时,您会发出事件,并且每个关心该数据的服务都会将其存储起来,以便在需要时使用。这是 Michael Bryzek 演讲以正确的方式设计微服务架构的关键。Michael Bryzek 是白标电子商务公司Flow.io的首席技术官。
如果您想要更深入的答案以及其他问题,我会为您指出我关于该主题的博客文章。