微服务架构设计模式笔记--第五章 微服务架构中的业务逻辑设计


企业应用程序的核心是业务逻辑,业务逻辑实现了业务规则。开发复杂的业务逻辑总是充满挑战。由于业务逻辑散布在多个服务上,因此在微服务架构中开发复杂的业务逻辑更具挑战性。
我们需要解决两个关键问题:

  • 典型的领域模型是由各种类(class)交织在一起的一个网络。虽然这在单体应用程序中不是问题,但在微服务架构中,类分散在不同的服务中,你需要避免跨越服务边界(也就是进程)的对象引用。
  • 设计在微服务架构下的业务逻辑,受到微服务下事务管理的种种约束,你的业务逻辑可以在一个服务内部使用ACID事务,服务之间的数据一致性必须使用Saga模式来维护。

幸运的是,我们可以使用领域驱动设计中的聚合模式(Aggregate)来解决这些问题。聚合模式下,服务的业务逻辑通过多个聚合组成的一个集合来体现。聚合是一组对象,可以作为一个单元来处理。在微服务架构中开发业务逻辑时,聚合可以起到以下两个重要的作用:

  • 使用聚合可以避免任何跨服务边界的对象引用,因为聚合之间通过主键进行引用,而不是通过对象的地址进行引用。
  • 由于单个事务只能创建或更新单个聚合,因此聚合满足微服务事务模型的约束。

因此,我们可以确保单个服务中的事务都满足ACID特性。

1. 业务逻辑的组织模式

在这里插入图片描述
图中显示了一个典型的服务架构。业务逻辑是六边形架构的核心。业务逻辑的周围是入站和出站适配器。入站适配器处理来自客户端的请求并调用业务逻辑。出站适配器被业务逻辑调用,然后它们再调用其他服务和外部应用程序。
业务逻辑通常是服务中最复杂的部分。在开发业务逻辑时,你应该以最适合应用程序的方式,精心地设计和组织业务逻辑。我确信大多数读者都经历过不得不维护别人的糟糕代码的挫败感。大多数企业应用程序都是用面向对象的语言编写的,例如Java,因此它们由类和方法组成。但是使用面向对象的语言并不能保证业务逻辑具有面向对象的设计。在开发业务逻辑时必须做出的关键决策是选用面向对象的方式,还是选用面向过程的方式。组织业务逻辑有两种主要模式:面向过程的事务脚本模式和面向对象的领域建模模式。

1.1 使用事务脚本模式设计业务逻辑

在这里插入图片描述
编写一个称为事务脚本的方法来处理来自表示层的每个请求,而不是进行任何面向对象的设计。如图所示,这种方法的一个重要特征是实现行为的类与存储状态的类是分开的。

  • 使用事务脚本模式时,脚本通常位于服务类中,在此示例中是 OrderService类。
  • 每个服务类都有一个用于请求或系统操作的方法。这个方法实现该请求的业务逻辑。
  • 它使用数据访问对象(DAO)访问数据库,例如 OrderDao。
  • 数据对象(在此示例中为Order类)是纯数据,几乎没有行为。

这种方法适用于简单的业务逻辑。但这往往不是实现复杂业务逻辑的好方法。

1.2 使用领域模型模式设计业务逻辑

如果业务逻辑变得复杂,你最终可能会得到噩梦般难以维护的代码。非是编写一个非常简单的应用程序,否则你应该抵制编写面向过程代码的诱惑,使用领域模型模式,并进行面向对象的设计。
在这里插入图片描述

领域模型:将业务逻辑组织为由具有状态和行为的类构成的对象模型。

服务方法通常很简单。因为服务方法几乎总是调用持久化领域对象。领域对象中包含大量的业务逻辑。例如,服务方法可以从数据库加载领域对象并调用其中一个方法。在这个例子中,order类具有状态和行为。此外,它的状态是私有的,只能通过它的方法间接访问。
使用面向对象设计有许多好处:

  • 设计易于理解和维护。它不是由一个完成所有事情的大类组成,而是由许多小类组成,每个小类都有少量职责。
  • 这些类都密切地反映了现实世界,这使得它们在设计中的角色更容易理解。
  • 面向对象设计更容易测试:每个类都可以并且应该能够被独立测试。
  • 面向对象的设计更容易扩展,因为它可以使用众所周知的设计模式,例如,策略模式和模板方法模式,这些设计模式定义了在不修改代码的情况下扩展组件的方法。

1.3 关于领域驱动设计

领域驱动设计(Domain-Driven Design,DDD)是对面向对象设计的改进,是开发复杂业务逻辑的一种方法。
领域驱动设计的子域概念有助于把应用程序分解为服务。使用DDD时,每个服务都有自己的领域模型,这就避免了在单个应用程序全局范围内的领域模型问题。子域和相关联的限界上下文的相关概念是两种战略性DDD模式。
DDD还有一些战术性模式,它们是领域模型的基本元素( building block)。每个模式都是一个类在领域模型中扮演的角色,并定义了类的特征。开发人员广泛采用的基本元素包括以下几种:

  • 实体(entity):具有持久化ID的对象。具有相同属性值的两个实体仍然是不同的对象。
  • 值对象(value object):作为值集合的对象。具有相同属性值的两个值对象可以互换使用。值对象的一个例子是Money类,它由币种和金额组成。
  • 工厂(factory):负责实现对象创建逻辑的对象或方法,该逻辑过于复杂,无法由类的构造函数直接完成。它还可以隐藏被实例化的具体类。工厂方法一般可实现为类的静态方法。
  • 存储库(repository):用来访问持久化实体的对象,存储库也封装了访问数据库的底层机制。
  • 服务(service):实现不属于实体或值对象的业务逻辑的对象。

还有一个被众人忽略的基本元素: 聚合

2. 使用聚合模式设计领域模型

传统领域模型缺少每个业务对象的明确边界。 缺乏边界有时会导致问题,尤其是在微服务架构中。

2.1 模糊边界所带来的问题

除了概念模糊之外,缺少明确的边界会在更新业务对象时导致问题。典型的业务对象具有一些不变量,即必须始终强制执行的业务规则。例如, Order具有最小订单金额。直接更新业务对象的一部分可能会导致违反业务规则。DDD聚合旨在解决此问题。

2.2 拥有明确的边界

聚合是一个边界内的领域对象的集群,可以将其视为一个单元。它由根实体和可能的一个或多个其他实体和值对象组成。许多业务对象都被建模为聚合。
在这里插入图片描述
聚合将领域模型分解为块,单独的每一块更容易理解。它们还阐明了加载、更新和删除等操作的范围。这些操作作用于整个聚合而不是部分聚合。聚合通常从数据库中完整加载,从而避免了延迟加载所导致的任何复杂性。删除聚合会从数据库中删除其所有对象。

  • 聚合代表了一致的边界
    更新整个聚合而不是聚合的一部分,可以解决前面例子中遇到的一致性问题。在聚合根上调用更新操作,这会强制执行各种不变量约束。此外,可以使用例如版本号或数据库级锁锁定聚合根来处理并发性。
  • 识别聚合是关键
    设计领域模型的关键部分是识别聚合,以及它们的边界和根。聚合内部结构的细节是次要的。聚合的价值不仅仅是帮助我们设计模块化的领域模型。更重要的是聚合必须遵守某些规则。

2.3 聚合的规则

  • 规则一:只引用聚合根
    只引用聚合根规则的目标是消除直接更新对象引发的业务规则失效问题。要求聚合根是聚合中唯一可以由外部类引用的部分。客户端只能通过调用聚合根上的方法来更新聚合。
  • 规则二:聚合间的引用必须使用主键
    使用标识(例如,主键)而不是对象引用意味着聚合是松耦合的。它确保聚合之间的边界得到很好的定义,并避免意外更新不同的聚合。此外,如果聚合是另一个服务的一部分,则不会出现跨服务的对象引用问题。
  • 规则三:在一个事务中,只能创建或更新一个聚合
    这个约束对于微服务架构来说是完美的。它可以确保单个事务的范围不超越服务的边界。此约束还满足大多数 NOSQL数据库的受限事务模型。

2.4 聚合的颗粒度

在开发领域模型时,你必须做出的关键决策是决定每个聚合的大小。

  • 聚合理想上应该很小。由于每个聚合的更新都是序列化的,因此更细粒度的聚合将提高应用程序能同寸处理的请求数量,从而提高可扩展性。
  • 改善用户体验,因为它降低了两个用户尝试同时更新一个聚合而引发冲突的可能性。
  • 聚合是事务的范围,可能需要定义更大的聚合以使特定的聚合更新操作满足事务的原子性。

2.5 使用聚合设计业务逻辑

在这里插入图片描述
在典型的(微)服务中,大部分业务逻辑由聚合组成。其余的业务逻辑存在于领域服务和Saga中。Saga编排本地事务的序列,以确保数据的一致性。服务是业务逻辑的入口,由入站适配器调用。服务使用存储库从数据库中检索聚合或将聚合保存到数据库。每个存储库都由访问数据库的出站适配器实现。图中显示了Order Service基于聚合设计的业务逻辑。

3. 发布领域事件

领域事件:聚合在被创建时,或发生其他重大更改时发布领域事件。

3.1 为什么需要发布变更事件

领域事件很有用,因为应用程序的其他协作方(比如客户端、其他应用程序或同一应用程序中的其他组件)通常有兴趣了解聚合的状态更改。以下是一些可能的场景:

  • 使用基于编排的Saga维护服务之间的数据一致性。
  • 通知维护数据副本的服务,源数据已经发生了更改。这种方法称为命令查询职责隔离(CQRS)。
  • 通过 Webhook或消息代理通知不同的应用程序,以触发下一步业务流程。
  • 按顺序通知同一应用程序的不同组件,例如,将 WebSocket消息发送到用户的浏览器或更新如 Elastic Search这样的文本数据库。
  • 向用户发送短信或电子邮件通知,告诉他们订单已发货、他们的医疗处方已准备就绪,或者他们的航班延误。
  • 监控领域事件以验证应用程序是否正常运行。
  • 分析领域事件,为用户行为建模。

在所有这些场景中,领域事件都是由应用程序数据库中聚合的状态更改所触发的。

3.2 什么是领域事件

在命名领域事件时,我们往往选择动词的过去分词。这样的命名能够明确表达事件的一些属性。领域事件的每个属性都是原始值或值对象。例如,OrderCreated事件类具有orderId属性。
领域事件通常还具有元数据,例如事件ID和时间戳。

3.3 事件增强

事件增强的方法是,事件包含接收方需要的信息。它简化了事件接收方,因为他们不再需要从发布事件的服务请求数据。

3.4 识别领域事件

事件风暴是快速创建领域模型的有效技术。事件风暴是一种以事件为中心的研讨会,用于理解复杂的领域。它的具体方法是:把领域专家聚集在一个屋子里,准备大量便笺和一个非常大的白板。事件风暴的结果是一个以事件为中心的领域模型,它由聚合和事件组成。
事件风暴包括三个主要步骤:

  1. 头脑风暴:请求领域专家集体讨论领域事件。领域事件由橙色便笺表示,这些便笺在白板上按照时间轴顺序摆放。
  2. 识别事件触发器:请求领域专家确定每个事件的触发器,例如:用户操作、外部系统、另一个领域事件、时间的流逝。
  3. 识别聚合:请求领域专家识别那些使用命令的聚合并发出相应的事件。

3.5 生成和发布领域事件

从概念上讲,领域事件由聚合负责发布。聚合知道其状态何时发生变化,从而知道要发布的事件。聚合可以直接调用消息传递API。这种方法的弊端在于,由于聚合不能使用依赖注入,所以消息传递API需要作为方法参数传递。这将把基础设施和业务逻辑交织在一起,是非常不可取的。
更好的方法是在聚合和调用它的服务(或类)之间分配职责。服务可以使用依赖注入来获取对消息传递API的引用,从而轻松发布事件。只要状态发生变化,聚合就会生成事件并将它们返回给服务。聚合可以通过几种不同的方式将事件返回给服务。

  • 在聚合方法的返回值中包括一个事件列表(首选)。
  • 聚合根在一个内部字段中累积保存事件。

如何可靠地发布领域事件

第3章我们讨论了如何可靠地发送消息,例如把消息发送作为本地数据库事务的一部分。领域事件也不例外。服务必须使用事务性消息来发布事件,以确保领域事件是作为更新数据库中聚合的事务的一部分对外发布。第3章中描述的 Eventuate tram框架实现了这样一种机制。它将事件插入到 OUTBOX表中,作为更新数据库的ACID事务的一部分。事务提交后,插入到 OUTBOX表中的事件将发布到消息代理。

3.6 消费领域事件

领域事件最终作为消息发布到消息代理,例如 Apache Kafka。领域事件的接收方可以直接使用事件代理的客户端API。但是使用更高级的API比较方便。推荐使用:Eventuate Tram框架。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值