事件协作和事件溯源


虽然事件确实无处不在,但受欢迎程度的增长似乎导致术语准确性的丧失。Apache Kafka 成为“事件总线”,所有异步消息都被声明为事件,使用事件流被声明为 Event Sourcing,像这样模糊不同的概念对架构讨论没有帮助。

术语Event Sourcing、Event Streaming和Event Collaboration值得仔细研究和区分。在解决这些问题之前,让我们先勾勒一下我们所处的环境。这篇文章是关于实现业务流程的应用程序,由各种子系统或服务组成。


事件定义
一个事件是一个事实。它描述了过去已经发生的事情。

这个简单的洞察力已经对软件架构产生了深刻的影响。比如说。对一个事件没有任何争论。当我收到一个事件时,没有必要去验证它,我不能怀疑它。它所描述的事情已经发生了。如果一个事件的接收者不喜欢它,或者不能处理它,这总是接收者的问题,而不是发送者的问题。

(在实践中,我见过这样的系统,事件的发布者试图跟踪接收者方面的成功处理,甚至可以在出现错误的情况下自己采取进一步的行动。这背后可能有良好的意图,但它是基于对事件语义的根本误解。使用事件驱动通信的一个重要动机就是要避免这种复杂性!)

以上对事件术语的定义是非常笼统的。在(微)服务之间的通信背景下,它可能会导致误解,因为它还包括UI事件(MouseClicked, ButtonPressed)、日志事件或物联网数据。在业务系统架构的背景下,我们应该更具体一点,将自己限制在与我们的业务领域相关的 "有趣 "的事件。让我们把这些称为 "领域事件"。

领域事件定义
领域事件描述了过去发生的一个有趣的事实,并在领域内产生影响。

领域事件完全没有失去它的事实特征,上述更一般的事件定义仍然适用。

事件流
领域事件起源于哪里?在一个服务中,业务对象的状态发生变化。该服务在内部坚持这种状态变化。如果它认为该状态变化与它的内部环境无关,它将通知感兴趣的外部世界该变化已经发生。为此,一个事件被发布在消息总线上。为此建立的技术被称为 "基于日志的消息总线",最著名的代表是Apache Kafka。在运行过程中,这导致了一个连续的事件流,其他服务可以订阅。因此,基于发布和订阅这些流的通信的架构模式被称为 "事件流"。

定义:事件流
将事件发布到其他服务可以订阅的通道上。

就像事件这个词本身一样,这是一个非常广泛的术语,事件流也被用于上述的日志和物联网数据的例子。对于商业应用的各个服务通过事件流相互通信这种更具体的情况,术语Event Collaboration已经确立(例如,见Ben Stopford的书《Designing Event-Driven Systems》,O'Reilly 2018)。

定义:事件协作
多个组件使用领域事件的Event Streaming进行通信。

当一个服务订阅了来自另一个服务的事件流时,它可以使用这个来生成发送者业务对象的内部表示。让我们来看看我们的简单例子中的PaymentService。它订阅了OrderCreated事件,并可以在内部存储订单的有趣部分。这样,它就可以记住每笔付款中哪些货物是要用它来支付的。在查询的情况下,PaymentService可以提供关于付款的额外信息,而不必在运行时依赖OrderService。

只要数据所产生的事件在总线上仍然可用,就可以丢弃本地表示,并从事件中重新创建。对于已经接触过事件源的读者来说,这听起来会很熟悉。但是,虽然这与它有一些共同点(从一系列的事件中获取数据),但我们不应该仅仅将其称为事件源。我们需要区分两个不同的视角:一个服务的内部和外部世界。让我们退一步,看看事件源的基本原理。

事件溯源
不是存储当前状态,而是存储导致该状态的事件序列。

事件源通常以 "命令-查询责任隔离 "的形式与额外的视图相结合,简称CQRS。在这里,读取和写入数据的数据模型被分开。在CQRS与事件源的结合中,事件日志作为写的一方。写入方的每次更新,都会有一个或多个投影被转换成其他格式。例如,这可以是一个关系模型,然后允许我们轻松地检索在所有付款中积累的数据--一个典型的查询模型。另一个投影也可以是为外部世界的这次更新生成一个事件,并在总线上发布。外部世界的事件不一定要和内部的事件完全一样

将内部事件投射到出去的事件流中是事件源和事件协作之间的桥梁。一个服务的内部状态变化会产生一个外界感兴趣的领域事件。它通过事件流发布给感兴趣的消费者。

附录1:事件的类型
事件的不同用途(一方面用于内部数据管理的事件源,另一方面用于基于事件流的服务间的事件协作)使Martin Fowler等人对不同类型的事件进行了分类。在他关于这个主题的文章中,他谈到了三类:事件源事件、通知事件和事件携带的状态转移。

事件源事件在这里应该占据一个特殊的位置。如上所述,这些与事件协作无关,而是与服务内部的持久化有关。事件源事件是允许对象的状态从一系列的事件中被完全重新创建的事件。

在协作领域,Fowler区分了事件通知(其本身不包含任何数据,除了对变化对象的引用)和事件携带的状态转移。

事件协作中的标准情况应该是事件携带的状态转移。在这里,与事件相关的数据是在事件中提供的。在支付方式改变的情况下,为了留在支付区域,这将是,例如,被指定为新支付方式的银行细节。

另一方面,一个通知事件,只说有东西被改变了。为了获得相关数据,必须从发布事件的服务中查询受影响对象的当前状态。这至少有两个缺点。

首先,如果发送的参考是指一个易变的对象,事件和数据之间的联系就会丢失。可以想象,当事件在接收器处被处理时,另一个变化已经发生了。也许一个订单被改变了,但随后不久就取消了。后来,当通知事件被接收并查询状态时,结果将是一个意外的状态(甚至是一个错误信息)。

此外,查询的必要性破坏了事件驱动架构的一个主要优势。我们希望通过减少运行时的依赖性来提高我们系统的可靠性。在运行时,PaymentService不应该依赖OrderService来回答任何请求。

通知事件应该只在选定的情况下使用。例如,如果事件的有效载荷太大,以至于在总线上发布它在技术上没有意义。通知事件中的引用应该总是指代一个不可变的对象。一个有用的用例是文件的生成。一旦文档的生成完成,就会发布一个带有静态文件的URL的通知。

回到事件携带的状态转移--这里是开发者或架构师的工作,以有意义的方式确定哪些数据应该被包含。只有实际改变的数据?还是被改变的完整对象?或者任何介于两者之间的东西?在Mathias Verraes的这篇帖子中可以找到关于这个问题的有趣想法。


banq注:领域事件其实是一个业务概念,不能将其归于”事件“这个大的概念下,容易与系统的各种事件混淆,事件风暴和事件溯源是针对领域事件,或者领域活动,在这里将domain event中的event翻译成”活动“可能更好,领域中发生的活动,非常直接简单,也区别与各种技术上EDA或数据分析中的事件概念,也与运维监控中的系统事件区分开来,事件活动是DDD最主要关心的事件。