大家好,又见面了,我是你们的朋友全栈君。
目录
领域驱动实践总结一:基本理论总结与分析
一、领域驱动设计两大设计:战略设计和战术设计
(一)战略设计
1.出发角度与目标
2.实现方式:事件风暴与模型确立(用例分析、场景分析和用户旅程分析)
3.用三步来划定领域模型和微服务的边界
(二)战术设计
1.出发角度与目标
2.实现方式:DDD 分层架构、整洁架构、CQRS 和六边形架构等 (我们采用DDD 分层架构)
3.代码模型强调两点:聚合之间的代码边界一定要清晰 一定要有代码分层的概念
二、理解和分析领域 子域 核心域 通用域 支撑域
(一)整体理解领域与子域的概念(以桃树生物学知识体系的建立为例来加深理解)
(二)理解核心域、通用域和支撑域的划分及具体目的
1.核心域
2.通用域
3.支撑域
4.划分核心域、支撑域和通用域的主要目标
三、理解和分析界限上下文,定义领域边界
(一)通用语言
(二)限界上下文
四、理解和分析实体和值对象
(一)对于实体的具体理解
1.实体的业务形态
2.实体的代码形态
3.实体的运行形态
4.实体的数据库形态
(二)对于值对象的具体理解
1.实体的业务形态
2.实体的代码形态
3.实体的运行形态
4.实体的数据库形态
5. 值对象的优势和局限
(三)对于实体与值对象关系的理解
1.基本的关系理解
2.不同场景下关系的不同
五、理解和分析聚合思想:聚合和聚合根
(一)对聚合的理解和分析
(二)对聚合根的理解和分析
(三)聚合、聚合根、实体、值对象的对比
(四)基本理解综合
六、理解很分析领域事件来解耦微服务
(一)领域事件的识别
(二)微服务内外的领域事件分析
1. 微服务内的领域事件
2. 微服务之间的领域事件
(三)领域事件总体架构
1. 事件构建和发布
2. 事件数据持久化
3. 事件总线 (EventBus)
4. 消息中间件
5. 事件接收和处理
(四)具体案例分析
参考书籍、文献和资料
领域驱动实践总结一:基本理论总结与分析
领域驱动设计DDD是一种设计思想,它可以同时指导中台业务建模和微服务设计(中台本质是业务模型,微服务是业务模型的系统落地),领域驱动设计强调领域模型和微服务设计的一体性,先有领域模型然后才有微服务,而不是脱离领域模型来谈微服务设计。
微服务拆分困境产生的根本原因:不知道业务或者微服务的边界到底在什么地方。
DDD 核心思想:通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
对于领域驱动设计的学习做的总结主要写三篇博客,主要包括三部分:基本理论总结与分析、架构分析与代码设计、具体应用设计分析,主要参考的资料为极客时间的欧创新架构师的《DDD》实战,其他参考书籍在文章下方的参考书籍中。
本次主要总结DDD基本理论总结与分析
一、领域驱动设计两大设计:战略设计和战术设计
领域驱动设计认为:开发团队应该从业务需求中提炼出统一语言,再基于统一语言建立领域模型,这个领域模型会指导程序设计及编码实现,最后,又通过重构来发现隐式概念,并运用设计模式改进设计与开发质量。
领域驱动设计的整个设计过程主要分为两个阶段:战略设计阶段与战术设计阶段。
(一)战略设计
1.出发角度与目标
从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
2.实现方式:事件风暴与模型确立(用例分析、场景分析和用户旅程分析)
DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。
事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
3.用三步来划定领域模型和微服务的边界
第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
(二)战术设计
1.出发角度与目标
从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
2.实现方式:DDD 分层架构、整洁架构、CQRS 和六边形架构等 (我们采用DDD 分层架构)
从战略设计向战术设计的实施过程中,我们会将领域模型中的领域对象与代码模型中的代码对象建立映射关系,将业务架构和系统架构进行绑定。当我们去响应业务变化调整业务架构和领域模型时,系统架构也会同时发生调整,并同步建立新的映射关系。
微服务架构模型有好多种,例如DDD 分层架构、整洁架构、CQRS 和六边形架构等,其核心理念都是为了设计出“高内聚低耦合”的架构,轻松实现架构演进。而 DDD 分层架构的出现,使架构边界变得越来越清晰,它在微服务架构模型中,占有非常重要的位置。
DDD 分层架构包含用户接口层、应用层、领域层和基础层。通过这些层次划分,我们可以明确微服务各层的职能,划定各领域对象的边界,确定各领域对象的协作方式。
3.代码模型强调两点:聚合之间的代码边界一定要清晰 一定要有代码分层的概念
根据 DDD 分层架构模型建立了标准的微服务代码模型,在代码模型里面,各代码对象各据其位、各司其职,共同协作完成微服务的业务逻辑。
第一点:聚合之间的代码边界一定要清晰。
聚合之间的服务调用和数据关联应该是尽可能的松耦合和低关联,聚合之间的服务调用应该通过上层的应用层组合实现调用,原则上不允许聚合之间直接调用领域服务。这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用。
第二点:一定要有代码分层的概念。
写代码时一定要搞清楚代码的职责,将它放在职责对应的代码目录内。应用层代码主要完成服务组合和编排,以及聚合之间的协作,它是很薄的一层,不应该有核心领域逻辑代码。领域层是业务的核心,领域模型的核心逻辑代码一定要在领域层实现。如果将核心领域逻辑代码放到应用层,你的基于 DDD 分层架构模型的微服务慢慢就会演变成传统的三层架构模型了。
二、理解和分析领域 子域 核心域 通用域 支撑域
(一)整体理解领域与子域的概念(以桃树生物学知识体系的建立为例来加深理解)
领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。
DDD 的领域就是这个边界内要解决的业务问题域。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
领域建模和微服务建设的过程和方法基本类似,其核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。
如何给桃树建立一个完整的生物学知识体系,它的研究过程如下:
—>确定研究对象,即研究领域,这里是一棵桃树
zyf@@@zyf 对应 DDD 的领域,研究的具体问题域
—>对研究对象进行细分,将桃树细分为器官,器官又分为营养器官和生殖器官两种。其中营养器官包括根、茎和叶,生殖器官包括花、果实和种子。
zyf@@@zyf 具体问题域划分为两大问题域,根、茎、叶、花、果实和种子等器官则是细分后的问题子域。
DDD 将领域细分为多个子域的过程。
—>对器官进行细分,将器官细分为组织。比如,叶子器官可细分为保护组织、营养组织和输导组织等。
zyf@@@zyf DDD 将子域进一步细分为多个子域。
—>对组织进行细分,将组织细分为细胞,细胞成为我们研究的最小单元。细胞之间的细胞壁确定了单元的边界。
zyf@@@zyf DDD 将子域进一步细分确定研究的最小边界。
可以把细胞(细胞核、线粒体、细胞膜等物质)理解为 DDD 的聚合,细胞内的这些物质就可以理解为聚合里面的聚合根、实体以及值对象等。
(二)理解核心域、通用域和支撑域的划分及具体目的
子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
公司在 IT 系统建设过程中,由于预算和资源有限,对不同类型的子域应有不同的关注度和资源投入策略,记住好钢要用在刀刃上。很多公司的业务,表面看上去相似,但商业模式和战略方向是存在很大差异的,因此公司的关注点会不一样,在划分核心域、通用域和支撑域时,其结果也会出现非常大的差异。
比方都是电商平台的淘宝、天猫、京东和苏宁易购,他们的商业模式是不同,在面对各个子域的划分上侧重点都不一样!
1.核心域
最重要的,决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力。
在公司领域细分、建立领域模型和系统建设时,我们就要结合公司战略重点和商业模式,找到核心域了,且重点关注核心域。
建议技术团队要将核心域的建设排在首位,最好是有绝对的掌控能力和自主研发能力!
2.通用域
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。
3.支撑域
既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。
4.划分核心域、支撑域和通用域的主要目标
通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。
三、理解和分析界限上下文,定义领域边界
在 DDD 领域建模和系统建设过程中,领域专家、产品经理、项目经理、架构师、开发经理和测试经理等对同样的领域知识,不同的参与角色可能会有不同的理解,避免交流障碍,DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。
通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。
(一)通用语言
在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。
通用语言包含术语和用例场景,并且能够直接反映在代码中。
- 通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;
- 而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
下面是一个微服务设计实例的部分数据(也是后面实践的应用案例分析),表格中的这些名词术语就是项目团队在事件风暴过程中达成一致、可用于团队内部交流的通用语言。
在这个表格里面我们可以看到,DDD 分析过程中所有的领域对象以及它们的属性都被记录下来了,除了 DDD 的领域对象,
DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。还记录了在微服务设计过程中领域对象所对应的代码对象,并将它们一一映射。
(二)限界上下文
领域专家、架构师和开发人员的主要工作就是通过事件风暴来划分限界上下文。
可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。
限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。
业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。
正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
四、理解和分析实体和值对象
(一)对于实体的具体理解
是一个具体类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。
对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。
具体从实体的业务形态、代码形态、运行形态和数据库形态去进一步理解:
1.实体的业务形态
领域模型中的实体是多个属性、操作或行为的载体。
在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。
实体和值对象是组成领域模型的基础单元。
2.实体的代码形态
代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。
在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
3.实体的运行形态
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。
可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。
4.实体的数据库形态
与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。
- 在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
- 在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。
- 有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。
(二)对于值对象的具体理解
《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。
值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。简单来说,值对象本质上就是一个集。在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。
举例:人员实体原本包括姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
具体从实体的业务形态、代码形态、运行形态、数据库形态和值对象的优势和局限去进一步理解:
1.实体的业务形态
本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。
而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
2.实体的代码形态
代码模型中,有这样两种形态:
- 如果值对象是单一属性,则直接定义为实体类的属性;
- 如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。
3.实体的运行形态
实体实例化后的 DO 对象的业务属性和业务行为非常丰富,但值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。
引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。以上面的代码为例:
引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。以上面的代码为例:
4.实体的数据库形态
DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。
值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。
在这块建议的具体做法是:
- 在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;
- 在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
以上面的代码为例,在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。
5. 值对象的优势和局限
值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。
这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。
在使用时要充分的考虑值对象的优缺点。
(三)对于实体与值对象关系的理解
1.基本的关系理解
实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。
DDD 提倡从领域模型设计出发,而不是先设计数据模型。传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多的时候就很容易陷入无穷无尽的复杂的数据库设计,领域模型就很容易被数据模型绑架。可以说,值对象的诞生,在一定程度上,和实体是互补的。
2.不同场景下关系的不同
同样的对象在不同的场景下,可能会设计出不同的结果。
有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。
而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
五、理解和分析聚合思想:聚合和聚合根
在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
(一)对聚合的理解和分析
聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。
聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。
(二)对聚合根的理解和分析
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
- 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
- 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
- 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。
(三)聚合、聚合根、实体、值对象的对比
聚合 | *高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但不建议你对微服务过度拆分。 *在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。 *一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。 |
聚合根 | *聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。 *一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。 |
实体 | *有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。 *状态可变,它依附于聚合根,其生命周期由聚合根管理。 *实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。 *实体可以引用聚合内的聚合根、实体和值对象。 |
值对象 | *无 ID,不可变,无生命周期,用完即扔。 *值对象之间通过属性值判断相等性。 *核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。 *值对象尽量只引用值对象。 |
(四)基本理解综合
实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。
六、理解很分析领域事件来解耦微服务
领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。
(一)领域事件的识别
我们在做事件风暴的时候,进行用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。
领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。
在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。
(二)微服务内外的领域事件分析
1. 微服务内的领域事件
当领域事件发生在微服务内的聚合之间,领域事件发生后完成事件实体构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。
微服务内大部分事件的集成,都发生在同一个进程内,进程自身可以很好地控制事务,因此不一定需要引入消息中间件。
但一个事件如果同时更新多个聚合,按照 DDD“一次事务只更新一个聚合”的原则,你就要考虑是否引入事件总线。
但微服务内的事件总线,可能会增加开发的复杂度,因此你需要结合应用复杂度和收益进行综合考虑。
微服务内应用服务,可以通过跨聚合的服务编排和组合,以服务调用的方式完成跨聚合的访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。
2. 微服务之间的领域事件
跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,其主要目的是实现微服务解耦,减轻微服务之间实时服务访问的压力。
跨微服务的事件可以推动业务流程或者数据在不同的子域或微服务间直接流转。
跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还可能需要考虑引入分布式事务机制等。
微服务之间的访问也可以采用应用服务直接调用的方式,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务,以确保数据的一致性。
分布式事务机制会影响系统性能,增加微服务之间的耦合,所以我们还是要尽量避免使用分布式事务。
(三)领域事件总体架构
领域事件总体技术架构图,领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。
1. 事件构建和发布
事件基本属性至少包括:事件唯一标识、发生时间、事件类型和事件源,其中事件唯一标识应该是全局唯一的,以便事件能够无歧义地在多个限界上下文中传递。
事件基本属性主要记录事件自身以及事件发生背景的数据。
事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。领域事件发生后,事件中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这种存储格式在消息中间件中也比较容易解析和获取。
事件发布之前需要先构建事件实体并持久化。
事件发布的方式有很多种,你可以通过应用服务或者领域服务发布到事件总线或者消息中间件,也可以从事件表中利用定时程序或数据库日志捕获技术获取增量事件数据,发布到消息中间件。
2. 事件数据持久化
事件数据持久化可用于系统之间的数据对账,或者实现发布方和订阅方事件数据的审计。
事件数据持久化有两种方案,在实施过程中你可以根据自己的业务场景进行选择:
- 持久化到本地业务数据库的事件表中,利用本地事务保证业务和事件数据的一致性。
- 持久化到共享的事件数据库中。这里需要注意的是:业务数据库和事件数据库不在一个数据库中,它们的数据持久化操作会跨数据库,因此需要分布式事务机制来保证业务和事件数据的强一致性,结果就是会对系统性能造成一定的影响。
3. 事件总线 (EventBus)
事件总线是实现微服务内聚合之间领域事件的重要组件,它提供事件分发和接收等服务。
事件总线是进程内模型,它会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传递数据。
事件分发流程大致如下:
如果是微服务内的订阅者(其它聚合),则直接分发到指定订阅者;
如果是微服务外的订阅者,将事件数据保存到事件库(表)并异步发送到消息中间件;
如果同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库(表),再异步发送到消息中间件。
4. 消息中间件
跨微服务的领域事件大多会用到消息中间件,实现跨微服务的事件发布和订阅。
5. 事件接收和处理
微服务订阅方在应用层采用监听机制,接收消息队列中的事件数据,完成事件数据的持久化后,就可以开始进一步的业务处理。
领域事件处理可在领域服务中实现。
(四)具体案例分析
领域事件是 DDD 的一个重要概念,在设计时我们要重点关注领域事件,用领域事件来驱动业务的流转,尽量采用基于事件的最终一致,降低微服务之间直接访问的压力,实现微服务之间的解耦,维护领域模型的独立性和数据一致性。
这里的具体用例结合后面的案例一起讲解,此处先预留。
参考书籍、文献和资料
1.极客时间课程《DDD实战》,欧创新,2019.
2.郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.
3.陈超、秦金卫、张逸等. 高可用可伸缩微服务架构. 电子工业出版社. 2019.
4.Eric Evans. 领域驱动设计-软件核心复杂性应对之道。 人民邮电出版社. 2018.
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/129980.html原文链接:https://javaforall.cn