异步API中事件、命令和状态区别


事件、命令、状态和时间序列值的区别:

事件:   

  • 用户已创建
  • ECS实例已启动

命令/说明/请求 :
  • 向用户 X 发送重置密码电子邮件
  • 从用户 Y 处收取 £x 的付款

状态  :  
  • 用户(完整对象)
  • 产品(完整的对象)
  • 订单(完整对象)

时间序列值
  • 股票价格
  • API 上的错误率指标
  • 时间序列数据当然是单个事物的状态,它是独特的,因为它具有周期性——无论它是否发生变化,都会发送更新。

事件与状态
事件表示某些事情已经发生或改变,例如“帐户已创建”。忽略时间戳和元数据,它可能具有如下有效负载:

{
  "EventType": "ACCOUNT_CREATED"
 
"AccountID": "8c0fd83f-ff3f-4e0e-af4b-2b7470334efa"
}

如果您想了解特定帐户的详细信息,那么您需要通过其他途径获取它,例如对帐户 REST API 的 HTTP 请求或任何现有的接口。

另一方面,状态包含已创建或更改的任何实体的完整状态。例如

{
  "EntityType": "ACCOUNT"
 
"ID": "8c0fd83f-ff3f-4e0e-af4b-2b7470334efa",
 
"Name": "David",
 
"Email": "An email@domain.com",
 
"Tel: {
   
"Type: "Mobile"
   
"Country Code": "44"
   
"777777777"
  }
  .....etc
}

在这个例子中,我没有加入一个字段来说明是创建还是更新。下游并不一定会关心他们是否看到了之前的信息,他们只会检查他们是否已经拥有了特定的实体。我们发送的是状态,而不是发生了什么,比如创建或更新。例外情况是需要特殊处理的删除,例如使用特殊的报文类型或空有效负载来表示状态已消失。

状态信息可用于多种场景,但在使用事件日志而非数据库作为真相来源的事件源路线时,状态信息则是必需的。

我在实践中发现,状态和事件之间的区别可能比目前建议的要模糊一些。你可能会遇到半途而废的解决方案,即事件包含一些常用信息(如电子邮件),但不包含更详细的信息。这样做虽然不是很纯粹,但对于只关心电子邮件的用户来说,可以节省大量的 API 请求。

与此类似,有时事件只传达一个字段的变化,例如 "电话号码已更改 "事件就包括电话号码和用户 ID,因此包含了所有状态。有时,状态信息可能包括前后状态或包含已更改字段名称/路径(如 changes=[firstname,person.phone.mobile])的更改列表。

内容比较:

  • 事件:一个 ID 和枚举来说明发生了什么  
  • 状态: 完整的数据实体

类型:

  • 事件:    标识发生的特定事件的枚举,例如 EMAIL_UPDATED  
  • 状态: 实体名称,如 PROFILE

后续调用:

  • 事件:需要后续调用,后续需要API调用 
  • 状态: 不需要

更新  :

  • 事件:  通过事件名称标识,但无法查看旧值    
  • 状态:如果状态是新的或更新的,通常不会传达,但有时消息会有旧的和新的状态

消息大小:

  • 事件:    小的
  • 状态:    中型或大型

事件与状态的关键区别:

  • 状态:以名词静态为主,当前状态,通常需要主语,主谓宾中的宾语是状态。
  • 事件:以动词为主,突出发生了什么事情,以谓语为主,可以没有主语。


权衡
具体权衡标准:

1、消费者数量
随着使用者数量的增加,有状态方法的优点是您不会在 API 上承受沉重的负载。
想象一下,总线上有 100 条消息,同时到达 15 个消费者。然后,您的 API 将在一两秒内收到 1500 个请求。

2、弹性
就消费者数量而言,如果您的 API 不是那么可靠,那么有状态选项可以更好地提高弹性,因为您不依赖消息总线和 API,只需依赖消息总线即可获取所有信息数据。

3、耦合
弹性和其他一些要点实际上是一种耦合形式。如果一个服务必须调用另一个服务的 API 来获取数据,那么它与该服务的耦合比状态消息解决方案更紧密,在状态消息解决方案中,消费者不需要了解生产者,也不依赖于其名称、弹性、API 模式等。

4、数据传输量
如果大多数消费者只需要 2 或 3 个字段,但状态消息中有 200 个字段,则可能会造成浪费。在这种情况下,假设同步 API(例如 REST、GraphQL)粒度更细,事件选项将更加高效。对于小型集中状态对象(例如 10-20 个字段)来说,这不是一个主要优点,但如果发送大约 10 KB 的大块数据,则更重要。

5、消费者的简单性
有时我听到有人断言,状态信息更简单,因为不需要调用应用程序接口(API)。但是......并不总是那么简单。最好的解释就是举个例子。请考虑以下情况:

  • 感兴趣的数据是用户的账户详情
  • 当账户属性发生变化(如电子邮件地址发生变化)时,出于安全考虑,您希望向用户发送电子邮件或短信
  • 您采用了状态信息的方式
  • 不包含更改列表,只包含当前状态。

任何负责发送电子邮件或短信的服务都必须有自己的状态,这样它才能比较前后的值,发现是电子邮件发生了变化,而不是姓名等其他字段发生了变化。

另一方面,如果您有一个 "电子邮件已更改 "的单一事件(事件中包含新邮件或可通过 API 获取),那么处理服务就可以是无状态的。

在这种情况下,使用事件的消费者实际上要简单得多,但状态信息的问题可以通过包含更改列表来解决。

6、数据结构管理
对于有状态方法(REST 和消息),您必须保持两个schema结构同步,与 API 框架相比,很多消息传递系统对schema管理的支持并不好。

7、聚合
如果一个服务需要几个实体来完成其工作(这些实体通常通过多条状态消息到达),那么事件模型可能会更简单。消费者接收到一个事件,然后立即进行几个 REST 调用或单个图形 QL 调用,以获取所需的实体来继续工作。

使用状态方法时,您可能需要处理顺序混乱的消息,并在继续处理之前等待所有消息。或者,必须构建一个包含所有实体的更大的聚合状态消息,这也有其自身的问题。

什么是指令/命令/请求
指令或命令是 "做 X "的请求。举例来说,一个商业网站或政府服务机构会在用户付款后通过邮局或快递送货。异步操作有两种方式:

  • 有一个交付微服务正在监听通用的 ORDER_PLACED 事件(或订单状态),并根据这些事件安排交付。
  • 订单应用程序(或消耗 ORDER_PLACED 事件的中间微服务)向配送公司服务写出 "PREPARE_DELIVERY "指令或类似指令。
  • 后者是指令的一个示例。

指令信息通常包含下游工作所需的全部信息,但也并非必须如此。一般来说,由于指令具有很强的针对性,因此没有理由不在报文中包含相关数据,除非需要任何最好不要在报文总线上传输的大文件或图像。

命令与状态以及事件的区别
在了解了命令之后,我们来比较一下状态和事件信息。我认为它们的区别在于

  • 状态或事件消息非常通用,可能有多个服务对其感兴趣。
  • 指令命令更具体,针对的是某个特定的用户,尽管耦合度较低(通过队列或类似方式)。
  • 对于命令,人们通常期望通过另一条消息得到响应,以确认该消息已被接收、接受或执行。

我个人对此的看法是,命令最适合工作流,在这种工作流中,你希望保持较低的耦合度,但无论如何,你都在请求某些事情发生,而且你关心它是否真的发生。您可能希望能够在仪表板上显示用户订单的状态及其交付情况,并在出现问题时采取行动。您不希望从众多系统中调取数据来获取该视图。这种情况通常需要一个协调器,如 Camunda 或 Uber Cadence 或 AWS Step Functions。

对于事件/状态信息,源系统(或协调器)在完成其工作后不承担任何责任。它只需发布一条消息,说明 "这里有一些新的/更新的数据",然后就可以继续工作了。其他服务则负责决定该做什么,并提供下游操作的状态视图。一个显而易见的推论是,在传输状态时,如果任何关键(业务功能)下游都依赖于它,那么消息传递系统就必须非常强大,因为源系统中没有重试或标记错误的机会。源系统不知道下行系统是否收到了数据,也不知道下行系统是否收到了数据并成功处理了它。

时间序列数据
在这里我不想多说,因为在信息中写什么的问题要明显得多:

  • 1 个或多个值、
  • 值类型
  • 时间戳。

所面临的挑战主要围绕信息总线和消费者,例如,确定在给定时间内所有数据何时到达(参见流媒体系统中的水印),以及在数据丢失风险与吞吐量和延迟之间找到适当的平衡。但在信息中加入什么内容的问题相对简单。

除了有效负载的内容之外,还应该考虑构成所有消息的标准信封的消息元数据。一些建议是:

ID
在消息中包含唯一的 ID,无论它是状态、命令等。我建议使用 UUID 来保证唯一性。该 ID 应该仅与消息有关,而不是与实体有关。这很有用,因为:

  • 对于命令,操作可能不是幂等的,例如发送电子邮件不是幂等的,因此您必须能够消除重复
  • 即使对于理想情况下幂等的状态,最好避免在消费者中重复工作,因此有一个 ID 来检查可以让这变得容易。

时间戳
在标准 UTC 格式中包含时间戳,以便消费者可以重新排序消息并清楚时间戳的含义。我建议这是基于写入源数据库(如果适用)或处理的实体,而不是消息发送时间,这在线程系统中可能是不确定的。
在格式上,这是有争议的,但我更喜欢时间戳的字符串版本,因为它使调试更容易,而无需转换纪元值。例如 2024-02-19T12:18:07.000Z,而不是 1708344420000。

版本控制
如果您希望能够轻松地将不同版本路由到不同的使用者,请制定版本控制计划,该计划可以位于版本字段中,也可以作为消息类型名称的一部分。
不要混淆消息信封(在许多实体之间共享)的版本和特定实体的版本。最好有 2 个版本号,每个版本号一个。

测试和环境
在消息中允许测试和多种环境是值得的。例如,考虑一个标志来说明消息是否是测试消息。这将允许轻松过滤生产中的测试数据,而不会污染您的分析系统。

还要考虑环境标志。将生产数据流入测试环境以帮助提供真实的数据是很常见的。有时您会想了解这一点,因为数据来自生产,引用的 ID 将不存在。标志可以让您知道这来自另一个环境,并且并非所有链接的数据都可能流入该测试环境。

例子
作为具有上述字段的消息的示例:

 {
  "messageID": "cc7b9901-c339-4c7d-80cd-c400f20581fd"
 
"timestamp": "2024-02-19T12:18:07.000Z"
 
"entityType": "ACCOUNT",
 
"envelopeVersion": 1,
 
"isTest": true,
 
"fromEnvironment": "prod"
 
"payload": {
   
"version" = 1,
   
"accountID": "0a0ebe8d-e48a-4195-8372-4f54c5dfd4e5",
  }
 }

最后的想法
我们已经了解了事件与状态的一些优缺点,并且还研究了命令,观察到后者经常用于工作流程中,在该工作流程中您关心指令的接收并希望了解操作的状态。它的后面。

具体就状态和事件而言,我不确定是否存在 100% 首选的方法,只是根据消费者数量、数据实体之间的关系进行权衡。如果我必须离开围栏,我想说的是,事实证明,国家信息往往比预期更复杂,所以在其他条件相同的情况下,我稍微倾向于事件。有几个原因:

  • 只有一个 API 用于获取数据 - 不需要保持 2 个同步
  • 消费者不必组装按随机顺序出现的物品
  • 可通过单一 API 访问的单一事实来源
  • 无需担心重播和回填 - 只需从 REST/GraphQL/RPC API 获取历史数据。

尽管如此,事件确实意味着服务之间的耦合更紧密,并且如果消费者数量很高,则不会总是扩展。

无论你做什么,都要有一个清晰的计划,尽量保持一致和合乎逻辑,不要意外地做出选择。换句话说,不要在没有任何明确推理的情况下随机混合服务中的指令、状态和事件。这并不意味着您应该尝试采用一种适合所有企业范围的模式。即使在单个域中,也可以让一个服务发出状态,而另一个服务监听该状态并在数据更改时发送命令以执行特定操作。