写点什么

DDD 领域驱动设计实战 (六)- 理解领域事件 (Domain Event)

作者:JavaEdge
  • 2021 年 12 月 19 日
  • 本文字数:4220 字

    阅读完需:约 14 分钟

DDD领域驱动设计实战(六)-理解领域事件(Domain Event)
  • 如何将领域事件建模成对象,何时应该为领域事件创建唯一的身份标识?

  • 哪些组件用于发布事件,哪些组件用于订阅事件

  • 为什么我们需要一个事件存储?如何实现事件存储、如何使用事件存储?

  • 如何通过不同的方式将领域事件发布给自治系统

1 when and why 使用领域事件?

1.1 定义

使用领域事件时,首先就是要对不同事件进行定义。


《领域驱动设计》并未给出领域事件的定义,因为该模型是在该书出版后才被提出。


当前对领域事件的定义:领域专家所关心的发生在领域中的一些事件。将领域中所发生的活动建模成一系列的离散事件。每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。


如何确定哪些事件对领域专家重要?

1.2 识别领域事件

  • “当……”

  • “如果发生……,则……”

  • “当做完……的时候,请通知……”这里的通知本身并不构成一个事件,只是表明我们需要向外界发出通知.


在这些场景中,若发生某种事件后,会触发进一步操作,则该事件很可能就是领域事件。有时从领域专家话中,好像也还看不出哪里有领域事件,但业务需求依然可能需要领域事件。领域专家有时可能意识不到这些需求,只有在经过跨团队讨论后才意识到这些。之所以会这样,是由于领域事件需发布到外部系统,如到另一个限界上下文。由于这样的事件由订阅方处理,它将对本地和远程上下文都产生影响。


由于领域事件需要发布到外部系统,如发布到另一个限界上下文。这样的事件由订阅方处理,影响本地和远程上下文。


一个领域事件将导致进一步业务操作,在实现业务解耦同时,还有助于形成完整的业务闭环。


领域事件可以是业务流程的一个步骤,如一个事件发生后触发的后续动作:密码连续输错三次,触发锁定账户的动作。

领域事件为何要用最终一致性,而非 SOA 直接调用?

因为聚合的一个原则:一个事务中最多只能更改一个聚合实例,所以:


  • 本地限界上下文中的其他聚合实例,可通过领域事件的方式同步

  • 用于使远程依赖系统与本地系统保持一致解耦本地系统和远程系,有助提高双方协作服务的可伸缩性

聚合创建并发布事件

  • 订阅方可先存储事件,然后再将其转发到远程订阅方

  • 或不经存储,直接转发除非 MQ 共享了模型的数据存储,不然即时转发需要 XA(两阶段提交)。


系统业务低峰期,批处理过程通常进行一些系统维护工作,如删除过期对象、创建新对象以支持新业务需求或通知用户所发生的重要事件。这样的批处理过程通常需复杂查询 &&庞大事务。若这些批处理过程存在冗余会怎样?


系统中发生的每一件事情,都用事件形式捕获,然后将事件发布给订阅方处理,能简化系统吗?


肯定的!它可消除先前批处理过程中的复杂查询,因为我们能够准确知道在何时发生何事,限界上下文也由此知道接下来应该做啥。在接收到领域事件时,系统可立即处理。原本批量集中处理的过程可以分散成许多粒度较小的处理单元,业务需求也由此更快满足,用户也可及时进行下一步操作。


领域事件驱动设计可切断领域模型之间的强依赖。事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,即可实现领域模型的解耦,维护领域模型的独立性和数据一致性。在领域模型映射到微服务架构时,领域事件可解耦微服务,微服务间的数据不必要求强一致性,而是基于事件的最终一致性。

触发领域事件

领域事件由外部命令触发。触发命令可以是领域服务,也可以是实体的某一个方法或者行为。

触发事件的用法

走 canal 增量同步数据库数据,通过监听特定表的数据变更来触发生成事件的调用。如此有利于主流业务的解耦,提高维护和可读性。(具体生成事件的操作当然还是放在对应领域的微服务中,canal 监听消费端可以理解为一个任务调度平台)。这样的实现逻辑相对简单。


那不同领域事件,如何处理呢?

3 处理领域事件

3.1 微服务内

领域事件发生在微服务内的聚合间,领域事件发生后完成事件实体的构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。


微服务内大部分事件的集成,都发生在同一进程,进程自身即可控制事务。但一个事件若同时更新多个聚合,按一次事务只更新一个聚合原则,可考虑引入事件总线。


微服务内应用服务,可通过跨聚合的服务编排和组合,以服务调用方式完成跨聚合访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。


在微服务内,不是说少用领域事件,而是推荐少用事件总线。DDD 是以聚合为单位进行数据管理,若一次操作会修改同一微服务内的多个聚合的数据,就需保证多个聚合的数据一致性。为了解耦不同聚合,需采用分布式事务或事件总线,而事件总线不太方便管理服务和数据的关系,可用类似 saga 之类的分布式事务技术。总之需确保不同聚合的业务规则和数据一致性。

3.2 微服务间

跨微服务的领域事件会在不同限界上下文或领域模型间实现业务协作,主要为解耦,减轻微服务间实时服务访问压力。


领域事件发生在微服务间较多,事件处理机制也更复杂。跨微服务事件可推动业务流程或数据在不同子域或微服务间直接流转。


跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、MQ,甚至事件数据持久化时还可能需考虑引入分布式事务。


微服务间访问也可采用应用服务直接调用,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务。分布式事务会影响系统性能,增加微服务间耦合,尽量避免使用。

5 领域事件设计

5.1 构建和发布

基本属性

至少包括如下:


  • 事件唯一标识(全局唯一,事件能够无歧义在多个限界上下文中传递)

  • 发生时间

  • 事件类型

  • 事件源


即主要记录事件本身以及事件发生背景的数据。

业务属性

记录事件发生那刻的业务数据,这些数据会随事件传输到订阅方,以开展后续业务操作。


事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。领域事件发生后,事件中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这种存储格式在消息中间件中也比较容易解析和获取。


为保证事件结构的统一,通常创建事件的基类,子类可自行继承扩展。由于事件没有太多业务行为,实现一般比较简单。


事件发布前需先构建事件实体并持久化。事件实体的业务数据推荐按需发布,避免泄露不必要业务信息。

事件发布方式

  • 可通过应用服务或者领域服务发布到事件总线或 MQ

  • 也可从事件表中利用定时程序或数据库日志捕获技术获取增量事件数据,发布到 MQ

5.2 事件数据持久化

意义

  • 系统之间数据对账

  • 实现发布方和订阅方事件数据的审计


当遇到 MQ、订阅方系统宕机或网络中断,在问题解决后仍可继续后续业务流转,保证数据一致性。毕竟虽然 MQ 都有持久化功能,但中间过程或在订阅到数据后,在处理之前出问题,需要进行数据对账,这样就没法找到发布时和处理后的数据版本。关键的业务数据推荐还是落库。

实现方案

  • 持久化到本地业务 DB 的事件表,利用本地事务保证业务和事件数据的一致性

  • 持久化到共享的事件 DB。业务、事件 DB 不在同一 DB,它们的数据持久化操作会跨 DB,因此需分布式事务保证业务和事件数据强一致性,对系统性能有影响

5.3 事件总线(EventBus)

意义

实现同一微服务内的聚合之间的领域事件,提供事件分发和接收等服务。是进程内模型,会在微服务内聚合之间遍历订阅者列表,采取同步或异步传递数据。


因为在微服务内部在同一个进程,事件总线相对好配置,它可以配置为异步的也可以配置为同步的。如果是同步就不需要落库。推荐少用微服务内聚合之间的领域事件,它会增加开发复杂度。而微服务之间的事件,在事件数据落库后,通过应用服务直接发布到 MQ。

事件分发流程

  • 若是微服务内的订阅者(其它聚合),则直接分发到指定订阅者

  • 微服务外的订阅者,将事件数据保存到事件库(表)并异步发送到 MQ

  • 同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库(表),再异步发送到 MQ

5.4 MQ

跨微服务的领域事件大多会用到 MQ,实现跨微服务的事件发布和订阅。虽然 MQ 自身有持久化功能,但中间过程或在订阅到数据后,在处理之前出问题,需要进行数据对账,这样就没法找到发布时和处理后的数据版本。关键的业务数据推荐还是落库。

5.5 接收 &&处理

微服务订阅方在应用层采用监听机制,接收 MQ 中的事件数据,完成事件数据的持久化后,就可以开始进一步的业务处理。领域事件处理可在领域服务中实现。


  • 事件是否被消费成功(消费端成功拿到消息或消费端业务处理成功),如何通知消息生产端?因为事件发布方有事件实体的原始的持久化数据,事件订阅方也有自己接收的持久化数据。一般可以通过定期对账的方式检查数据的一致性。

  • 在采取最终一致性的情况下,事件消费端如果出现错误,消费失败,但之前的业务都成功了,虽然记录了 event dB,但后续如何处理,人工介入吗?如果人工介入再解决,前端用户会不会看到数据不一致,体验不好?失败的情况应该比例是很少的。失败的信息可采用多次重试,如果这个还解决不了,只能将有问题的数据放到一个问题数据区,人工解决。当然要确保一个前提,要保证数据的时序性,不能覆盖已产生的数据。


一般发布方不会等待订阅方反馈结果。发布方有发布的事件表,订阅方有消费事件表,可采用对账方式发现问题数据。

管理

大型系统的领域事件有很多:


  • 做好源端和目的端数据的对账处理,发现并识别处理过程中的异常数据异步的方式一般都有源端和目的端定期对账的机制。比如采用类似财务冲正的方式。如果在发布和订阅之间事件表的数据发现异步数据有问题,需要回退,会有相应的代码进行数据处理,不过不同的场景,业务逻辑会不一样,处理的方式会不一样。有的甚至还需要转人工处理。

  • 发现异常数据后,要有相应的处理机制

  • 选择适合自己场景的技术,保证数据正确传输

6 总结

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


领域事件驱动机制可实现一个发布方 N 个订阅方的模式,这在传统的直接服务调用设计中基本是不可能做到的。

领域事件 V.S CQRS

CQRS 主要是想读写分离,将没有领域模型的查询功能,从命令中分离出来。领域事件主要目的还是为了微服务解耦,在连续的业务处理过程中,以异步化的方式完成下一步的业务处理,降低微服务之间的直连。它们的共同点就是通过消息中间件实现从源端数据到目的端数据的交互和分离。


如果你就是不想用领域事件,聚合之间还可以通过应用层来协调和交互。应用服务是所有聚合之上的服务,负责服务的组合和编排,以及聚合之间的协调。


参考

  • 《实现领域驱动设计》

  • 《领域驱动设计》

发布于: 3 小时前阅读数: 8
用户头像

JavaEdge

关注

正在征服世界的 Javaer。 2019.09.25 加入

曾就职于百度、携程、华为等大厂,阿里云开发者社区专家博主、腾讯云+社区2019、2020年度最佳作者、慕课网认证作者、CSDN博客专家,简书优秀创作者兼《程序员》专题管理员,牛客网著有《Java源码面试解析指南》。

评论

发布
暂无评论
DDD领域驱动设计实战(六)-理解领域事件(Domain Event)