Skip to content

领域事件:解耦微服务的关键

在领域建模时,我们发现除了命令和操作等业务行为以外,还有一类非常重要的事件。这类事件发生后通常会触发进一步的业务操作,在DDD中这类事件被称为领域事件(Domain Event)。

领域事件

领域事件是领域模型非常重要的一部分,用于表示领域中发生的事件。一个领域事件往往会导致进一步的业务操作,它在实现领域模型解耦的同时,还有助于形成完整的业务操作闭环。

举例来说,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可以是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发缴费邮件通知操作;还可以是一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

那在领域建模时,如何识别和捕捉这些领域事件呢?

在用户旅程或者场景分析时,我们需要捕捉业务人员、需求分析人员以及领域专家口中的这些具有前后动作关系的关键词,比如:"如果发生......,则......""当做完......时,请通知......""发生......时,则......"等。在这些业务场景中,如果发生某种事件后,会触发进一步的业务操作,那么这个事件很可能就是领域事件。

领域事件采用事件驱动架构(Event-Driven Architecture,EDA)设计,可以切断领域模型之间的强依赖关系,在领域事件发布后,事件发布方不必关心订阅方的事件处理是否成功。这样就可以实现领域模型的解耦,维护领域模型的独立性。当领域模型映射到微服务时,领域事件就可以解耦微服务,这时微服务之间的数据就可以不再要求强一致性,而是基于最终一致性。

再回到具体的业务场景,我们发现有的领域事件发生在微服务内的聚合之间,有的发生在微服务之间,还有两者皆有的场景。一般来说,跨微服务的领域事件会相对较多。在微服务设计时,不同场景下的领域事件的处理方式会不同。

与采用同步服务调用实现数据强一致性的机制不同,领域事件一般都会结合消息中间件和事件发布订阅的异步处理方式,实现数据最终一致性。

那么,领域事件处理为什么要采用最终一致性,而不是强一致性呢?

我们先一起回顾一下聚合有一个重要设计原则:"在边界之外使用最终一致性。"如果在一次事务提交中,修改的数据超出了一个聚合的边界,简单点说就是一笔交易,如果同时涉及多个聚合的数据更新,那么就可以采用数据最终一致性。

微服务内的领域事件

在微服务内发生领域事件,如果同时更新多个聚合数据时,你需要确保多个聚合数据的一致性。按照DDD"一次事务只更新一个聚合"的原则,你可以引入事件总线(Event Bus),通过事件总线来实现微服务内多聚合数据的最终一致性,或者采用事务机制保证数据强一致性。

在采用事件总线进行领域事件处理时,可以根据需要完成领域事件实体的构建和事件数据持久化,然后发布方聚合会将领域事件数据发布到事件总线,由订阅方聚合接收领域事件数据后完成后续业务处理。事件总线的设计方式,可能会增加微服务开发的复杂度,需要结合应用的复杂度和收益进行综合考虑。

你可能会问,在同一个微服务内,为什么一次事务更新多个聚合数据时,要用事件总线或事务机制呢?

这是因为聚合是微服务内最小的业务功能单元。为了保证聚合内数据更新时符合聚合内固定的业务规则,在一次事务提交时通常会将聚合内所有变更的对象数据作为整体,通过聚合领域服务或聚合根方法一次通过仓储完成数据持久化操作。如果在一次交易中需要同时更新多个聚合数据,那么每一个聚合就是一个独立的数据提交单元,我们需要确保多个聚合数据都能在这个交易中成功提交并更新,以保证不同聚合数据的一致性。而基于事件总线的异步化机制,就可以保证微服务内聚合之间数据提交时的最终一致性。

如果不采用事件总线的最终数据一致性机制,其实你也可以采用事务机制保证数据强一致性。比如在应用服务中增加事务控制,在对多个聚合的领域服务进行组合和编排时,通过事务机制来确保多个聚合在提交数据时实现数据强一致性。这种方式一般应用于实时性和数据一致性要求高的业务场景,但采用事务机制可能会出现系统性能损耗。

微服务之间的领域事件

跨微服务的领域事件可以在不同限界上下文,或领域模型之间实现业务协作,其主要目的是实现微服务解耦,推动业务流程或者数据在不同子域或微服务之间流转。同时也可以减轻微服务之间同步服务访问的压力,避免当某个关键微服务无法提供服务时,出现雪崩效应。

领域事件发生在微服务之间的场景比较多,事件处理的机制也更加复杂。微服务之间的领域事件可以采用异步化的最终一致性设计。设计时要总体考虑领域事件的构建、发布和订阅、领域事件数据的持久化、消息中间件,甚至在事件数据持久化时可能还需要引入分布式事务等机制。

微服务之间的领域事件,也可以采用同步服务调用的强一致性设计,实现实时的数据和服务访问。其弊端就是需要引入分布式事务机制,以确保微服务之间的数据强一致性。但分布式事务机制会影响系统性能,同时增加微服务之间的耦合。

所以,应尽量减少微服务之间的同步服务调用方式,优先采用基于消息中间件的最终一致性设计。

领域事件案例

下面介绍一个与保险承保业务有关的领域事件的案例,以加深对领域事件的理解。

一个保单的生成,通常会经历很多业务子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多领域事件,这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的流转和角色转换,如不同微服务或聚合之间的实体与值对象之间的转换。我列出了几个关键流程,用来说明如何用领域事件驱动设计来驱动保险业务流程。

事件起点:客户购买保险,业务人员完成投保单录入,生成投保单,启动缴费动作。

1)投保微服务生成缴费通知单,发布第一个事件:缴费通知单已生成。将缴费通知单事件数据发布到消息中间件。收款微服务订阅缴费通知单事件,完成缴费操作。缴费通知单已生成领域事件结束。

2)收款微服务缴费完成后,发布第二个领域事件:缴费已完成。将缴费事件数据发布到消息中间件。原来的事件订阅方收款微服务这时则变成了事件发布方。原来的发布方投保微服务这时转换为缴费已完成事件的订阅方。投保微服务在收到缴费信息并确认缴费完成后,完成投保单转成保单的操作。缴费已完成领域事件结束。

3)投保微服务在投保单转保单完成后,发布第三个领域事件:保单已生成。将保单事件数据发布到消息中间件。保单微服务接收到保单数据后,完成保单数据保存操作。保单已生成领域事件结束。

4)保单微服务完成保单数据保存后,后面还会发生一系列的领域事件,以并发的方式将保单事件数据通过消息中间件分别发送到佣金、收付和再保等微服务,一直到财务,完成保单后续所有业务流程。这里就不详细展开了。

综上,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同业务领域和微服务之间的流转,实现领域模型和微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。

领域事件驱动实现机制

领域事件的执行需要一系列组件和技术来支撑。下面我们来看一下领域事件的总体技术架构,领域事件处理包括:领域事件构建和发布、领域事件数据持久化、事件总线、消息中间件、事件接收和处理等。

  1. 事件构建和发布

在领域事件发生后,我们需要记录领域事件的基本信息,用于管理领域事件数据、持久化和事件数据传输。

事件实体的基本属性至少应该包含事件唯一标识、发生时间、事件类型和事件源,其中,事件唯一标识应该是全局唯一的,以便事件通过ID能够无歧义地在多个限界上下文之间传递。事件ID的唯一性和时序性的特点,通过主键约束可以避免事件消息的重复消费。事件基本属性主要记录事件自身以及事件发生背景相关的数据,如事件发生时间、事件发生源等。

另外,事件实体中还有一项最重要的属性,那就是业务属性,用于记录事件发生那一刻领域事件相关的业务数据。这些业务数据会随着事件数据的发布而传输到订阅方,以便订阅方在接收到事件数据后开展下一步业务操作。

领域事件的基本属性和业务属性一起构成事件实体。领域事件发生后,领域事件的业务数据经过加工后被写入事件实体中,写入后的业务数据就不可以更改了。如果业务数据在业务逻辑处理时发生了变更,就会产生新的领域事件,这时新的业务数据会被写入新的事件实体。

基于事件实体中业务数据不可更改的特点,可以将这些业务数据转换为JSON串或者XML格式,以序列化值对象的形式存储在事件表中,这种存储格式在消息中间件中也比较容易解析和获取。

为了统一领域事件类数据结构,我们创建了领域事件基类DomainEvent。领域事件子类可以根据具体的业务场景来扩充属性和方法。由于事件实体没有太多的业务行为,所以其实现方法一般都比较简单。

领域事件发布之前先构建事件实体,对于关键的不允许丢失和需要对账的事件数据,在事件发布之前需先持久化到数据库事件表中。

事件发布可以在应用服务或领域服务中完成,将领域事件数据发布到事件总线(微服务内)或者消息中间件(微服务之间);也可以采用定时程序或数据库日志捕获技术,从数据库事件表中获取增量事件数据,发布到消息中间件。具体选择什么样的实现方式,需要根据具体的业务场景来选择。

  1. 事件数据持久化

对于关键业务的事件数据,在业务数据写入数据库后,需要同步完成事件数据的持久化操作。事件数据持久化主要用于系统之间的数据对账,或者发布方和订阅方事件数据的审计。通过事件数据持久化,当遇到消息中间件、订阅方系统宕机或者网络中断等问题时,在问题解决后仍可获取事件数据继续后续业务流转,保证数据传输的连续性和一致性。

事件数据持久化方案有两种,你可以权衡自己的技术需求和业务场景后做出选择。

1)持久化到本地业务数据库的事件表中,利用本地事务保证业务和事件数据的一致性。

2)持久化到共享的事件数据库中。这里需要注意的是:由于业务和事件数据不在同一个数据库中,因此持久化时会跨数据库,需要分布式事务机制来保证数据的强一致性,这可能会对系统性能造成一定影响。

  1. 事件总线

事件总线是实现微服务内聚合之间领域事件传输的重要技术组件,提供事件分发和接收等服务。事件总线是进程内模式,它会在微服务内聚合之间遍历订阅者列表。你可以通过事件总线配置,选择同步或异步模式传递数据。

事件分发流程大致如下。

1)如果是微服务内的订阅者(其他聚合),则直接分发到指定订阅者。

2)如果是微服务外的订阅者,首先将事件数据保存到事件库(表),然后异步发送到消息中间件。

3)如果同时存在微服务内和微服务外订阅者,则先分发到内部订阅者,将事件数据保存到事件库(表),再异步发送到消息中间件。

  1. 消息中间件

跨微服务的领域事件大多会用到消息中间件,完成跨微服务的事件发布和订阅,实现数据最终一致性。

消息中间件一般分为源端发布者和目的端订阅者。源端发布者微服务完成特定主题的消息发布,对于重要的业务数据还需要有持久化的事件表,记录事件基本数据以及事件推送和处理状态。另外,源端发布者微服务还需要有消息补偿机制,在目的端出现故障和消息不可达时,可以完成数据重传。目的端订阅者微服务订阅特定消息主题完成进一步业务处理。

消息中间件主要有两种消息发布机制:应用逻辑推送和数据库数据增量推送。

▪应用逻辑推送机制是在发布者微服务中产生领域事件时,通过应用实现逻辑完成业务数据和事件数据持久化,同时在应用逻辑中直接将源端事件数据推送至消息中间件,完成事件发布的过程。这个过程需要引入事务机制,确保业务逻辑和消息发布的数据强一致性。

▪数据库数据增量推送机制是在事件数据完成持久化后,通过数据库日志捕获技术(Change Data Capture,CDC)获取事件增量数据,并将事件增量数据推送到消息中间件,完成事件发布的过程。这样,应用处理逻辑与领域事件处理逻辑相互独立,有利于实现业务处理和事件发布处理两者的逻辑解耦。

CDC和消息中间件的结合,也可以用于同构或异构数据库的数据复制和同步,通过削峰填谷策略平衡源端和目的端数据处理速度的差异。

目前,消息中间件的产品和技术解决方案非常成熟,市场上可选的技术组件也非常多,如Kafka、RabbitMQ等,你可以根据企业的具体情况选择合适的消息中间件。

  1. 事件接收和处理

源端发布者微服务完成领域事件发布后,订阅者微服务在应用层采用监听机制,接收从消息中间件订阅的特定主题的事件数据。订阅者微服务在进行事件数据持久化时,可以以事件实体的ID为主键,通过主键约束规避对事件数据的重复消费。订阅者微服务完成事件数据持久化后,就可以开始进一步的业务处理了。订阅者微服务基于事件数据完成业务处理后,修改持久化事件表中的事件状态数据,同时,也可将事件处理结果推送至消息中间件反馈队列,将结果反馈给发布者微服务。

如果订阅者微服务在事件处理结束后,又产生了新的领域事件,这时还需要其他微服务完成下一步的业务处理。那么,这时订阅者微服务将转变为发布者微服务的角色,参考上文消息中间件的发布逻辑完成消息发布即可。

领域事件的订阅逻辑一般建议在应用层实现,领域事件的业务处理逻辑一般建议在领域层的领域服务中实现。

领域事件运行机制

下面以承保业务流程的缴费通知单事件为例解释跨微服务的领域事件的运行机制,这个领域事件发生在投保和收款微服务之间。发生的领域事件是:缴费通知单已生成。下一步的业务操作是:缴费。

事件起点:出单员生成投保单,核保通过后,发起缴费操作。

第一步,投保微服务应用服务,调用聚合中的领域服务createPaymentNotice和createPaymentNoticeEvent,分别创建缴费通知单、缴费通知单事件。其中缴费通知单事件类PaymentNoticeEvent继承基类DomainEvent。

第二步,利用仓储服务持久化缴费通知单相关的业务和事件数据。为了避免产生分布式事务,这些业务和事件数据都持久化到本地投保微服务数据库中。

第三步,通过数据库日志捕获技术或者定时程序,从数据库事件表中获取事件增量数据,发布到消息中间件。注意:事件发布也可以通过应用服务或者领域服务完成。

第四步,收款微服务在应用层从消息中间件订阅缴费通知单事件消息主题,监听并获取事件数据后,应用服务调用领域层的领域服务将事件数据持久化到本地数据库中。

第五步,收款微服务调用领域层的领域服务PayPremium,完成缴费。

第六步,事件结束。

提示:缴费完成后,后续流程的微服务还会产生很多新的领域事件,比如缴费已完成、保单已生成等。这些事件处理的基本流程与上述第一步到第六步的处理机制类似,这里不再赘述。

小结

领域事件是DDD的一个重要概念,在设计时我们要重点关注领域事件,用领域事件来驱动业务的流转,尽量采用基于事件的最终一致性设计,降低微服务之间直接访问的压力,实现微服务的解耦,维护领域模型的独立性。

领域事件驱动中的消息中间件设计模式是很成熟的技术方案,在很多分布式架构中得到了大量的应用。除此之外,领域事件驱动机制还可以实现一个发布方N个订阅方的模式,这在传统的同步服务调用设计中基本是不可能做到的。

Released under the MIT License.