在领域建模时,我们发现除了命令和操作等业务行为以外,还有一类非常重要的事件。这类事件发生后通常会触发进一步的业务操作,在DDD中这类事件被称为领域事件(Domain Event)。
那到底什么是领域事件?领域事件驱动设计的技术实现机制是怎样的?我们这一章将重点讲解这两个问题。
1 领域事件
领域事件是领域模型非常重要的一部分,用于表示领域中发生的事件。一个领域事件往往会导致进一步的业务操作,它在实现领域模型解耦的同时,还有助于形成完整的业务操作闭环。
举例来说,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可以是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发缴费邮件通知操作;还可以是一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。
那在领域建模时,如何识别和捕捉这些领域事件呢?
在用户旅程或者场景分析时,我们需要捕捉业务人员、需求分析人员以及领域专家口中的这些具有前后动作关系的关键词,比如:“如果发生⋯⋯,则⋯⋯”“当做完⋯⋯时,请通知⋯⋯”“发生⋯⋯时,则⋯⋯”等。在这些业务场景中,如果发生某种事件后,会触发进一步的业务操作,那么这个事件很可能就是领域事件。
领域事件采用事件驱动架构(Event-Driven Architecture,EDA)设计,可以切断领域模型之间的强依赖关系,在领域事件发布后,事件发布方不必关心订阅方的事件处理是否成功。这样就可以实现领域模型的解耦,维护领域模型的独立性。当领域模型映射到微服务时,领域事件就可以解耦微服务,这时微服务之间的数据就可以不再要求强一致性,而是基于最终一致性。
再回到具体的业务场景,我们发现有的领域事件发生在微服务内的聚合之间,有的发生在微服务之间,还有两者皆有的场景。一般来说,跨微服务的领域事件会相对较多。在微服务设计时,不同场景下的领域事件的处理方式会不同。
与采用同步服务调用实现数据强一致性的机制不同,领域事件一般都会结合消息中间件和事件发布订阅的异步处理方式,实现数据最终一致性。
那么,领域事件处理为什么要采用最终一致性,而不是强一致性呢?
我们先一起回顾一下第8章的内容,聚合有一个重要设计原则:“在边界之外使用最终一致性。”如果在一次事务提交中,修改的数据超出了一个聚合的边界,简单点说就是一笔交易,如果同时涉及多个聚合的数据更新,那么就可以采用数据最终一致性。
1.1 微服务内的领域事件
在微服务内发生领域事件,如果同时更新多个聚合数据时,你需要确保多个聚合数据的一致性。按照DDD“一次事务只更新一个聚合”的原则,你可以引入事件总线(Event Bus),通过事件总线来实现微服务内多聚合数据的最终一致性,或者采用事务机制保证数据强一致性。
在采用事件总线进行领域事件处理时,可以根据需要完成领域事件实体的构建和事件数据持久化,然后发布方聚合会将领域事件数据发布到事件总线,由订阅方聚合接收领域事件数据后完成后续业务处理。事件总线的设计方式,可能会增加微服务开发的复杂度,需要结合应用的复杂度和收益进行综合考虑。
你可能会问,在同一个微服务内,为什么一次事务更新多个聚合数据时,要用事件总线或事务机制呢?
这是因为聚合是微服务内最小的业务功能单元。为了保证聚合内数据更新时符合聚合内固定的业务规则,在一次事务提交时通常会将聚合内所有变更的对象数据作为整体,通过聚合领域服务或聚合根方法一次通过仓储完成数据持久化操作。如果在一次交易中需要同时更新多个聚合数据,那么每一个聚合就是一个独立的数据提交单元,我们需要确保多个聚合数据都能在这个交易中成功提交并更新,以保证不同聚合数据的一致性。而基于事件总线的异步化机制,就可以保证微服务内聚合之间数据提交时的最终一致性。
如果不采用事件总线的最终数据一致性机制,其实你也可以采用事务机制保证数据强一致性。比如在应用服务中增加事务控制,在对多个聚合的领域服务进行组合和编排时,通过事务机制来确保多个聚合在提交数据时实现数据强一致性。这种方式一般应用于实时性和数据一致性要求高的业务场景,但采用事务机制可能会出现系统性能损耗。
1.2 微服务之间的领域事件
跨微服务的领域事件可以在不同限界上下文,或领域模型之间实现业务协作,其主要目的是实现微服务解耦,推动业务流程或者数据在不同子域或微服务之间流转。同时也可以减轻微服务之间同步服务访问的压力,避免当某个关键微服务无法提供服务时,出现雪崩效应。
领域事件发生在微服务之间的场景比较多,事件处理的机制也更加复杂。微服务之间的领域事件可以采用异步化的最终一致性设计。设计时要总体考虑领域事件的构建、发布和订阅、领域事件数据的持久化、消息中间件,甚至在事件数据持久化时可能还需要引入分布式事务等机制。
微服务之间的领域事件,也可以采用同步服务调用的强一致性设计,实现实时的数据和服务访问。其弊端就是需要引入分布式事务机制,以确保微服务之间的数据强一致性。但分布式事务机制会影响系统性能,同时增加微服务之间的耦合。
所以,应尽量减少微服务之间的同步服务调用方式,优先采用基于消息中间件的最终一致性设计。
2 领域事件案例
下面介绍一个与保险承保业务有关的领域事件的案例,以加深对领域事件的理解。
一个保单的生成,通常会经历很多业务子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多领域事件,这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的流转和角色转换,如不同微服务或聚合之间的实体与值对象之间的转换。在图9-1中,我列出了几个关键流程,用来说明如何用领域事件驱动设计来驱动保险业务流程。
事件起点:客户购买保险,业务人员完成投保单录入,生成投保单,启动缴费动作。
1)投保微服务生成缴费通知单,发布第一个事件:缴费通知单已生成。将缴费通知单事件数据发布到消息中间件。收款微服务订阅缴费通知单事件,完成缴费操作。缴费通知单已生成领域事件结束。
2)收款微服务缴费完成后,发布第二个领域事件:缴费已完成。将缴费事件数据发布到消息中间件。原来的事件订阅方收款微服务这时则变成了事件发布方。原来的发布方投保微服务这时转换为缴费已完成事件的订阅方。投保微服务在收到缴费信息并确认缴费完成后,完成投保单转成保单的操作。缴费已完成领域事件结束。
3)投保微服务在投保单转保单完成后,发布第三个领域事件:保单已生成。将保单事件数据发布到消息中间件。保单微服务接收到保单数据后,完成保单数据保存操作。保单已生成领域事件结束。
4)保单微服务完成保单数据保存后,后面还会发生一系列的领域事件,以并发的方式将保单事件数据通过消息中间件分别发送到佣金、收付和再保等微服务,一直到财务,完成保单后续所有业务流程。这里就不详细展开了。
图1 保险承保过程中的领域事件分析
综上,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同业务领域和微服务之间的流转,实现领域模型和微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。