事件驱动架构中事件的双重性质


鉴于事件在事件驱动架构中扮演着如此重要的角色,人们对事件中应包含的内容却缺乏一致的看法。这可能源于这样一个事实:根据你的观点,事件可以实现不同的目的。

在遵循当代风格的事件驱动架构的系统中,微服务通过发出和订阅事件进行协作。
(请注意,本文仅讨论从一个域“发布”供其他域订阅的事件。不涉及例如如果您的持久性方法是事件溯源时使用的内部事件。)

在这些事件驱动的系统中,在服务之间传播的事件具有双重作用:它们触发动作并携带数据。

原则上,服务发出的事件:事件通常既是数据的触发器,又是数据的载体——所包含的数据量有所不同。

  • 纯触发器:其中所有信息都包含在事件类型中。
  • DTO数据传输:更改的实体/聚合的所有属性都将包含在事件中。

大家对这些事件定义没有共识:在互联网的其他地方,你也会发现它们被称为胖事件上帝事件RESTful 事件状态事件

“具有 DDD 背景的软件工程师/架构师”观点
作为从事事件驱动微服务的开发人员,主要关注的是将业务流程实现为事件流。

您将事件视为触发器,并希望为不同的触发器设置不同类型的事件。这样您就可以查看一系列事件并了解正在发生的事情。

拥有不同类型的事件也符合事件风暴等设计流程。便签上记录了发生的事情(事件类型),您无需在上面写数据。

使用不同的、正确命名的事件类型意味着应用通用语言。查看技术事件,即使是业务人员也能了解发生了什么。

你所实施的流程就是故事,事件是故事的最小单位。

如果你只有一种类型的事件,例如BookingUpdated,你必须通过查看哪些数据发生了变化来弄清楚发生了什么。

假设你的流程是购买电影票。如果你查看事件的顺序,你想看到什么?

  1. SeatSelected → PaymentReceived → TicketIssued或者
  2. BookingUpdated → BookingUpdated → BookingUpdated

毕竟,这是关于服务之间的协作——事件驱动不是数据复制。

从这个角度来看,对于任何实体,您都会发出不同类型的事件,事件类型清楚地表明发生了什么。就事件中包含的数据而言,它只是与事件相关的属性(在事件上下文中发生变化的属性)。

  • 如果您使用Kafka,则将与同一类实体相关的所有不同事件发布到一个主题。(要按顺序读取与同一实体相关的事件,它们必须位于同一分区。位于同一主题是位于同一分区的先决条件。)
  • 如果您使用schema registry,则使用RecordNameStrategyTopicRecordNameStrategy

这是完全合法的,而且会奏效。但你也应该考虑不同的观点。

“数据工程师”的观点
对于数据工程师来说,数据就是数据。数据不是表格,而是流,但最终它代表的是事物的状态。

如果数据的单位太小,数据团队就需要做更多工作来最终创建可用的表来表示所代表实体的状态。

如果流中只有一种类型的事件,那么这种方法效果最好,因为主题上的所有事件都共享相同的架构。这为您提供了“表流二元性”。此外,它还可以轻松地将流提取到数据库中,或提取到某种无头数据格式(例如Iceberg)中。

从数据的角度来看,如果您可以像查询数据库一样查询流,那么您会对永远保留最新状态的流感到满意。事实上,您实际上宁愿只拥有一个流基础设施和一个数据库,而不是同时拥有一个。(我认为流数据库解决了这个问题,但还没有真正研究过这个问题。而且有新的流产品,如Tektite,它允许您,我引用:“查询任何流或表中的数据,就像它是数据库表一样”。)

如果您使用 Kafka,则每个主题只能发布一种类型的事件。如果您使用schema registry,则可以使用TopicNameStrategy

那么该怎么办呢?
如果您在设计活动时仅注重其中一个目的而忽略另一个目的,那么您以后的生活可能会变得更加困难。

如果只关注数据视角,就会丢失有关事件原因的重要信息。不要将事件协作简化为数据复制。

话虽如此,您可能会遇到实际上只是数据复制的情况,并且需要广泛的事件。这包括

  • 使用事件来填充您的数据仓库或数据湖。
  • 引导稍后添加到系统中的新服务,这些新服务需要完整的事件历史记录才能开始使用最新数据。
  • 其他服务持有需要更新的数据的本地投影的情况(但更新本身不会触发操作)。

如果您只关注事件的触发性质,并且这些用例在产品生命周期的后期出现,则可能必须引入广泛事件作为附加事件。如果您尽早考虑到数据方面,则可以避免增加工作量。

那么,事件到底应该包含哪些内容?
我的看法是:

  • 事件必须包含其原因(上下文),即其所代表的商业(业务)事件。
  • 它至少必须包含此事件中发生更改的数据。
  • 相当数量的额外数据不会造成任何损害。如果您的实体可以序列化为一个仍然足够小的事件,请包含其状态的完整快照,并让您(以及您的数据消费者)的生活更轻松。

“包含完整快照”需要进一步限定。仍然要留意您包含的事件数据。这不仅是因为从技术角度来看,事件应该很小,这样它们才能快速复制,例如在分布式消息代理的多个节点之间。但更重要的是:您的事件流是一个 API。您需要像为 RESTful HTTP API 设计 JSON 对象一样仔细地设计事件。其中的内容很难删除,并且您希望能够在一定程度上在内部更改域模型而不影响事件负载。

因此,我的标准方法是使用精心设计的广泛事件,并在事件中包含原因(或作为标题)。虽然我希望了解原因,并通过查看事件顺序来了解故事,但不一定必须将其编码为事件类型。

那么序列将会是这样的

BookingUpdated(Reason: SeatSelected) → BookingUpdated(Reason: PaymentReceived) → BookingUpdated(Reason: TicketIssued)

对我来说,这体现了双重性质,对我来说,这是一种“两全其美”的做法。