02-DDD核心概念

领域、子域、核心域、通用域和支撑域。

0.1. 领域和子域

领域是从事一种专门活动或事业的范围、部类或部门。

领域具体指一种特定的范围或区域。

领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。

在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围

DDD 的研究方法与自然科学的研究方法类似。

在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。当所有问题子域完成研究时,就建立了全部领域的完整知识体系了。

image

  • 第一步:确定研究对象,即研究领域,一棵桃树。桃树的知识体系是已经确定要研究的问题域,对应 DDD 的领域
  • 第二步:对研究对象进行细分,将桃树细分为器官。根、茎、叶、花、果实和种子等器官则是细分后的问题子域。这个过程就是 DDD 将领域细分为多个子域的过程。
  • 第三步:将器官细分为组织。这个过程就是 DDD 将子域进一步细分为多个子域的过程。
  • 第四步:将组织细分为细胞,细胞成为研究的最小单元。细胞之间的细胞壁确定了单元的边界,也确定了研究的最小边界。

细胞核、线粒体、细胞膜等物质共同构成细胞,这些物质一起协作让细胞具有这类细胞特定的生物功能。在这里把细胞理解为 DDD 的聚合,细胞内的这些物质就可以理解为聚合里面的聚合根实体以及值对象等,在聚合内这些实体一起协作完成特定的业务功能。这个过程类似 DDD 设计时,确定微服务内功能要素和边界的过程。

每一个细分的领域都会有一个知识体系,也就是 DDD 的领域模型。在所有子域的研究完成后,我们就建立了全域的知识体系,也就建立了全域的领域模型

0.1.1. 例子-保险业

为实现保险领域建模和微服务建设,根据业务关联度以及流程边界将保险领域细分为:承保、收付、再保以及理赔等子域,而承保子域还可以继续细分为投保、保全(寿险)、批改(财险)等子子域。

在投保这个限界上下文内可以建立投保的领域模型,投保的领域模型最后映射到系统就是投保微服务。这就是一个保险领域的细分和微服务的建设过程。

领域建模和微服务建设的过程和方法基本类似,其核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度

0.2. 核心域、通用域和支撑域

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据重要性功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。

  • 核心域:决定产品和公司核心竞争力的子域,是业务成功的主要因素和公司的核心竞争力。
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域。(比如认证、权限等,没有企业特点限制,不需要做太多的定制化。)
  • 支撑域:是必需的功能子域,既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域。(例如数据代码类的数据字典等系统。)

在不同的场景下,不同的人对桃树核心域的理解是不同的,因此对桃树的处理方式也会不一样。园丁更关注桃树花期的营养,而果农则更关注桃树落果期的营养,有时为了保证果实的营养供给,还会裁剪掉疯长的茎和叶(通用域或支撑域)。同样的道理,公司在 IT 系统建设过程中,由于预算和资源有限,对不同类型的子域应有不同的关注度和资源投入策略,好钢要用在刀刃上。

很多公司的业务,表面看上去相似,但商业模式和战略方向是存在很大差异的,因此公司的关注点会不一样,在划分核心域、通用域和支撑域时,其结果也会出现非常大的差异。

在领域细分、建立领域模型和系统建设时,要结合公司战略重点和商业模式,找到且重点关注核心域。

转型微服务架构,要将核心域的建设排在首位且有绝对的掌控能力和自主研发能力,如果资源实在有限的话,可以在支撑域或通用域上,暂时采用外购的方式。

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

0.3. 限界上下文

  • 通用语言:定义上下文含义,能够简单、清晰、准确描述业务涵义和规则
  • 限界上下文:定义领域边界,确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。

0.3.1. 通用语言

通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中:

  • 名词可以给领域对象命名,如商品、订单等,对应实体对象;
  • 动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。

通用语言贯穿 DDD 的整个设计过程。作为团队沟通和协商形成的统一语言,基于它,能够开发出可读性更好的代码,将业务需求准确转化为代码设计。

从事件风暴建立通用语言到领域对象设计和代码落地的完整过程如下图所示。

image

在事件风暴的过程中,建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个团队统一语言的过程。

通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。

微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。

设计过程中可以用表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。比如,领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码模型对象的映射关系等。

image

DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。

0.3.2. 限界上下文(context)

语言都有语义环境,通用语言也有上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。

将限界上下文拆解为两个词:限界和上下文。

  • 限界:是领域的边界
  • 上下文:是语义环境

通过领域的限界上下文,可以在统一的领域边界内用统一的语言进行交流。

限界上下文,用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

限界上下文是用来细分领域,从而定义通用语言所在的边界。

同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。领域边界就是通过限界上下文来定义的

0.3.3. 限界上下文与微服务的关系

理论上限界上下文就是微服务的边界。将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。

限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。

除了理论,微服务的拆分还是有很多限制因素的,在设计中不宜过度拆分。

限界上下文划分主要依据是业务的语义边界,比如客户的环境下只说客户相关的事情,权限环境下只定义权限相关的业务逻辑。

对于陌生业务环境,经过分析,基本能够知道有哪些流程和场景,这些流程和场景里应该有对应的语义环境。

而在具体的分析过程中,在确定一个子域,并完成事件风暴后,可以找出实体和聚合,实体和聚合根他们有业务属性和逻辑。基本知道这个聚合可以作什么样的业务,如果多个聚合共同完整这类业务,就可以把多个聚合放在一个限界上下文内,这样一个限界上下文就形成了。

0.4. 实体和值对象

在战略设计向战术设计过渡的这个过程中,理解和区分实体和值对象在不同阶段的形态是很重要的,毕竟阶段不同,它们的形态也会发生变化,这与我们的设计和代码实现密切相关。

0.4.1. 实体

在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。

0.4.1.1. 实体的业务形态

在 DDD 不同的设计过程中,实体的形态是不同的。

  • 在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。
  • 在事件风暴中,可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。

实体和值对象是组成领域模型的基础单元

0.4.1.2. 实体的代码形态

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。

0.4.1.3. 实体的运行形态

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

0.4.1.4. 实体的数据库形态

与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据库持久化对象。

在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。

在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。

而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。

  • 比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。
  • 比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。

0.4.2. 值对象

《实现领域驱动设计》一书中值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。

在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换。它可以和其它值对象进行相等性比较,且不会对协作对象造成副作用。

值对象本质上就是一个集合。这个集合里面有若干个用于描述目的、具有整体概念和不可修改的属性。

在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。

0.4.2.1. 举例

image

人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎。将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象。

0.4.2.2. 值对象的业务形态

值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。

  • 本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。
  • 值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。

值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。

在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。

0.4.2.3. 值对象的代码形态

值对象在代码中有这样两种形态。

  • 如果值对象是单一属性,则直接定义为实体类的属性
  • 如果值对象是属性集合,则把它设计为类或结构体,将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用
type Person struct{
    id string       // 值对象,人员唯一主键
    name string     // 单一属性值对象
    age int         // 单一属性值对象
    gender bool     // 单一属性值对象
    address Address // 属性集值对象,被实体引用
}

type Address struct{
    province string // 值对象,无主键
    city string     // 值对象
    country string  // 值对象
    street string   // 值对象
}

0.4.2.4. 值对象的运行形态

  • 实体实例化后的领域对象的业务属性和业务行为非常丰富
  • 值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为很少

值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是:

  • 属性嵌入的方式:引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入

image

  • 序列化大对象的方式:引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入,比如,人员实体可以有多个通讯地址,多个地址序列化后可以嵌入人员的地址属性。

image

值对象创建后就不允许修改了,只能用另外一个值对象来整体替换

0.4.2.5. 值对象的数据库形态

DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。

传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。

值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。

基于上述人员和地址那个场景,实体和数据模型设计通常有两种解决方案:

  • 第一是把地址值对象的所有属性都放到人员实体表中,创建人员实体,创建人员数据表,第一个方案会破坏地址的业务涵义和概念完整性
  • 第二是创建人员和地址两个实体,同时创建人员和地址两张表,第二个方案增加了不必要的实体和表,需要处理多个实体和表的关系,从而增加了数据库设计的复杂性

在领域建模时,把地址作为值对象,人员作为实体,保留地址的业务涵义和概念完整性。在数据建模时,将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。值对象就是通过这种方式,简化了数据库设计。

在领域建模时,将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,将值对象嵌入实体,减少实体表的数量,简化数据库设计。

有 DDD 专家认为,要想发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也不用大惊小怪,只要业务能够顺利运行,就没什么关系。

0.4.2.6. 值对象的优势与局限

  • 优势是可以简化数据库设计,提升数据库性能
  • 值对象使用不当,优势就会变成劣势

值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。

值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。

0.4.3. 实体与值对象的关系

实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。值对象和实体在某些场景下可以互换,根据团队的设计和开发习惯,以及上面的优势和局限分析,选择最适合的方法。

DDD 提倡从领域模型设计出发,而不是先设计数据模型。传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多的时候就很容易陷入无穷无尽的复杂的数据库设计,领域模型就很容易被数据模型绑架。值对象的诞生,在一定程度上,和实体是互补的。

同样的对象在不同的场景下,可能会设计出不同的结果。

  • 有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候可以将地址设计为值对象,比如收货地址。
  • 在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。

0.5. 聚合和聚合根

在事件风暴中,根据业务操作和行为找出实体或值对象,进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文中,并在限界上下文内完成领域建模。

0.5.1. 聚合

在 DDD 中,实体和值对象是很基础的领域对象。

  • 实体一般对应业务对象,它具有业务属性和业务行为
  • 值对象主要是属性集合,对实体的状态和特征进行描述

实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

0.5.2. 聚合根

聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  • 作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 在聚合之间,是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。

聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

0.5.3. 设计聚合

DDD 领域建模通常采用事件风暴,采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。

判断一个实体是否是聚合根,可以结合以下场景分析:

  • 是否有独立的生命周期?
  • 是否有全局唯一 ID?
  • 是否可以创建或修改其它对象?
  • 是否有专门的模块来管这个实体?

根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。

在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。

多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。

0.5.3.1. 聚合的设计原则

  1. 在一致性边界内建模真正的不变条件:聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
  2. 设计小聚合:如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
  3. 通过唯一标识引用其他聚合:聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
  4. 在边界之外使用最终一致性:聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
  5. 通过应用层实现跨聚合的服务调用:为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

0.5.3.2. 聚合特点

高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但不建议对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。

一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合,微服务的架构演进也就不再是一件难事了。

聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。

0.5.3.3. 实体的特点

有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。

0.5.3.4. 值对象的特点

无 ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。

0.6. 领域事件

在事件风暴时,除了命令和操作等业务行为以外,还有一种非常重要的事件,这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。

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

0.6.1. 识别领域事件

领域事件的定义是强关联的。在做用户旅程或者场景分析时,要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。

聚合的一个设计原则:在边界之外使用最终一致性。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。

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

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

0.6.1.1. 微服务内的领域事件

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

微服务内大部分事件的集成,都发生在同一个进程内,进程自身可以很好地控制事务,因此不一定需要引入消息中间件。

但一个事件如果同时更新多个聚合,按照 DDD“一次事务只更新一个聚合”的原则,就要考虑是否引入事件总线。

但微服务内的事件总线,会增加开发的复杂度,因此需要结合应用复杂度和收益进行综合考虑。

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

0.6.1.2. 微服务之间的领域事件

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

领域事件发生在微服务之间的场景比较多,事件处理的机制也更加复杂。

跨微服务的事件可以推动业务流程或者数据在不同的子域或微服务间直接流转。跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还可能需要考虑引入分布式事务机制等。

微服务之间的访问也可以采用应用服务直接调用的方式,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务,以确保数据的一致性。

分布式事务机制会影响系统性能,增加微服务之间的耦合,所以还是要尽量避免使用分布式事务。

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

0.6.2. 领域事件总体架构

领域事件的执行需要一系列的组件和技术来支撑。领域事件总体技术架构如下图,领域事件处理包括:

  • 事件构建和发布
  • 事件数据持久化
  • 事件总线
  • 消息中间件
  • 事件接收和处理等

image

0.6.2.1. 事件构建和发布

事件基本属性至少包括:

  • 事件唯一标识(全局唯一):使事件能够无歧义地在多个限界上下文中传递
  • 发生时间
  • 事件类型
  • 事件源

事件基本属性主要记录事件自身以及事件发生背景的数据。

还有:

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

事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。

领域事件发生后,事件中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这种存储格式在消息中间件中也比较容易解析和获取。

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

image

事件发布之前需要先构建事件实体并持久化。

事件发布的方式有很多种:

  • 通过应用服务或者领域服务发布到事件总线或者消息中间件
  • 从事件表中利用定时程序或数据库日志捕获技术获取增量事件数据,发布到消息中间件

0.6.2.2. 事件数据持久化

事件数据持久化可用于系统之间的数据对账,或者实现发布方和订阅方事件数据的审计。

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

事件数据持久化有两种方案:

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

注意:业务数据库和事件数据库不在一个数据库中,它们的数据持久化操作会跨数据库,因此需要分布式事务机制来保证业务和事件数据的强一致性,结果就是会对系统性能造成一定的影响。

0.6.2.3. 事件总线

事件总线是实现微服务内聚合之间领域事件的重要组件,它提供事件分发和接收等服务。

事件总线是进程内模型,它会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传递数据。

事件分发流程大致如下:

  • 如果是微服务内的订阅者(其它聚合),则直接分发到指定订阅者;
  • 如果是微服务外的订阅者,将事件数据保存到事件库(表)并异步发送到消息中间件;
  • 如果同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库(表),再异步发送到消息中间件。

0.6.2.4. 消息中间件

跨微服务的领域事件大多会用到消息中间件,实现跨微服务的事件发布和订阅。消息中间件的产品非常成熟,市场上可选的技术也非常多,比如 Kafka,RabbitMQ 等。

0.6.2.5. 事件接收和处理

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

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

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

上次修改: 14 April 2020