DDD领域驱动设计实战(六)

Wesley13
• 阅读 467

点击上方“JavaEdge”,关注公众号

设为“星标”,好文章不错过!

===

1 定义

将领域中所发生的活动建模成一系列的离散事件。每个事件都用领域对象来表示。

领域事件是领域模型的组成部分,表示领域中所发生的事情。
一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

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

===

2 识别领域事件的话术

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

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

  • “发生……时,则……”等

  • “如果发生 这样的事情,它并不重要;如果发生那样的事情,它就很重要了”

在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。由于领域事件需要发布到外部系统,比如发布到另一个限界上下文。由于这样的事件由订阅方处理,它将对本地和远程上下文产生深远的影响。

那领域事件为什么要用最终一致性,而不是传统SOA的直接调用?

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

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

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

DDD领域驱动设计实战(六)
聚合创建并发布事件。订阅方可以先存储事件,然后再将其转发到远程订阅方,或不经存 储,直接转发。除非MQ共享了模型的数据存储,不然即时转发需要XA。

考虑在系统非高峰时期,批处理过程通常进行一些系统维护工作,比如删除过期对象、创建新对象以支持新业务需求或通知用户所发生的重要事件。这样的批处理过程通常需复杂 查询且需庞大事务支持。若这些批处理过程存在冗余会怎么样?
系统中发生的每一件事情,我们都用事件形式捕获,然后将事件发布给订阅方处理,能简化系统吗?

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

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

有的领域事件发生在微服务内的聚合之间,有的则发生在微服务之间,还有两者皆有的场景,一般来说跨微服务的领域事件处理居多。在微服务设计时不同领域事件的处理方式会不一样。

===

3 微服务内

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

微服务内大部分事件的集成,都发生在同一进程,进程自身可很好控制事务,因此不一定需要引入MQ。但一个事件若同时更新多个聚合,按“一次事务只更新一个聚合”原则,可考虑引入事件总线。

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

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

===

4 微服务间

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

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

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

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

===

5 领域事件总体架构

DDD领域驱动设计实战(六)


5.1 事件构建和发布

DDD领域驱动设计实战(六)

DDD领域驱动设计实战(六)

事件基本属性

DDD领域驱动设计实战(六)

至少包括:

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

  • 发生时间

  • 事件类型

  • 事件源

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

DDD领域驱动设计实战(六)

业务属性

DDD领域驱动设计实战(六)

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

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

为保证事件结构的统一,还会创建事件基类 DomainEvent,子类可以扩充属性和方法。由于事件没有太多的业务行为,实现方法一般比较简单。

事件发布之前需要先构建事件实体并持久化。事件发布的方式有很多种

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

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


5.2 事件数据持久化

DDD领域驱动设计实战(六)

DDD领域驱动设计实战(六)

意义

DDD领域驱动设计实战(六)

  • 系统之间数据对账

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

当遇到MQ、订阅方系统宕机或网络中断,在问题解决后仍可继续后续业务流转,保证数据一致性。

DDD领域驱动设计实战(六)

实现方案

DDD领域驱动设计实战(六)

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

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


5.3 事件总线(EventBus)

DDD领域驱动设计实战(六)

DDD领域驱动设计实战(六)

意义

DDD领域驱动设计实战(六)

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

事件分发流程

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

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

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


5.4 MQ

DDD领域驱动设计实战(六)

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


5.5 事件接收和处理

DDD领域驱动设计实战(六)

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

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

===

6 总结

今天我们主要讲了领域事件以及领域事件的处理机制。领域事件驱动是很成熟的技术,在很多分布式架构中得到了大量的使用。领域事件是DDD的一个重要概念,在设计时我们要重点关注领域事件,用领域事件来驱动业务的流转,尽量采用基于事件的最终一致,降低微服务之间直接访问的压力,实现微服务之间的解耦,维护领域模型的独立性和数据一致性。

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

参考

  • 《实现领域驱动设计》

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

  • 《领域驱动设计》

往期推荐

[

DDD领域驱动实战-子域/核心域等核心概念

](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUzNTY5MzA3MQ%3D%3D%26mid%3D2247487824%26idx%3D1%26sn%3De07b74823e47a5921849c20dfce492ff%26chksm%3Dfa80c330cdf74a26aeff09beb2702f3f75c9443437bdcf3f4f87051430acdf5ee9dd2bb693fe%26scene%3D21%23wechat_redirect)

[

实战DDD领域驱动设计之限界上下文

](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUzNTY5MzA3MQ%3D%3D%26mid%3D2247487843%26idx%3D1%26sn%3Daf2a5694bac7d9796ce6914cb67cdb97%26chksm%3Dfa80c303cdf74a15c970b412f11d189fb0d617c9f437dda022086ace6ef165254b72b59b92ee%26scene%3D21%23wechat_redirect)

[

DDD领域驱动设计实战(三)- 理解实体

](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUzNTY5MzA3MQ%3D%3D%26mid%3D2247487870%26idx%3D1%26sn%3De50fab89c2efb78b8c40a761d1b712d2%26chksm%3Dfa80c31ecdf74a0820360fff4376e98fb2f46eefae118143465d98d146e485dd8380510df73b%26scene%3D21%23wechat_redirect)

[

DDD领域驱动设计实战(四)- 理解值对象

](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUzNTY5MzA3MQ%3D%3D%26mid%3D2247487899%26idx%3D1%26sn%3Dea06dd7521ce632e754c25605d051e00%26chksm%3Dfa80c3fbcdf74aedd31db393b21a6c4d6c4d37b32ef8e0beeac6b41a987b6e20f22690616c06%26scene%3D21%23wechat_redirect)

[

架构设计之高可扩展性

](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUzNTY5MzA3MQ%3D%3D%26mid%3D2247487956%26idx%3D1%26sn%3Dc850896838d36e70dc1574a370b8aae8%26chksm%3Dfa80c3b4cdf74aa279bbaddccf4cc145a38f60eda8ba06cda8c17fb52169f9de9397fdb43d61%26scene%3D21%23wechat_redirect)

DDD领域驱动设计实战(六)

目前交流群已有 **800+**人,旨在促进技术交流,可关注公众号添加笔者微信邀请进群

DDD领域驱动设计实战(六)

喜欢文章,点个“在看、点赞、分享”素质三连支持一下~

本文分享自微信公众号 - JavaEdge(Java-Edge)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
2年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这