《微服务架构设计模式》读书笔记 | 第5章 微服务架构中的业务逻辑设计
前言
这是一本关于微服务架构设计方面的书,这是本人阅读的学习笔记。首先对一些符号做些说明:
()为补充,一般是书本里的内容;
[]符号为笔者笔注;
1. 业务逻辑组织模式
组织业务逻辑有两种主要的模式:面向过程的事务脚本模式和面向对象的领域建模模式。
1.1 一个典型的服务架构
业务逻辑周围是入站和出站适配器。
- 入站适配器:处理来自客户端的请求并调用业务逻辑;
- 出站适配器:被业务逻辑调用,然后它们再调用其他服务和外部程序应用;
Order Service具有六边形架构:
此服务由业务逻辑和以下适配器组成:
- REST API adapter:入站适配器,实现REST API,这些API会调用业务逻辑;
- OrderCommandHandlers:入站适配器,它接受来自消息代理的出站适配器,并调用业务逻辑;
- Database Adapter:由业务逻辑调用以访问数据库的出站适配器;
- Domain Event Publishing Adapter:将事件发布到消息代理的出站适配器;
1.2 使用事务脚本模式设计业务逻辑
事务脚本:将业务逻辑组织为面向过程的事务脚本的集合,每种类型的请求都有一个脚本。
特点:
- 实现行为的类与存储状态的类是分开的;
- 脚本通常在服务类中;
- 每个服务都有一个用于请求或系统操作的方法;这个方法实现请求的业务逻辑;
1.3 使用领域模型模式设计业务逻辑
领域模型:将业务逻辑组织为由具有状态和行为的类构成的对象模型。
特点:
- 服务方法通常很简单(因为服务方法几乎总是调用持久化领域对象,这些对象中包含大量业务逻辑);
- 如:Order类具有状态和方法,状态是私有的,只能通过它的方法间接访问;
- 易于理解、维护、测试和扩展;
1.4 关于领域驱动设计
领域驱动设计(DDD)是对面向对象设计的改进,是开发复杂业务逻辑的一种方法。其基本元素如下:
- 实体(entity):具有持久化ID的对象。具有相同属性值的两个实体仍然是不同的对象;
- 值对象(value object):作为值集合的对象。具有相同属性值的两个值对象可以互换使用;
- 工厂(factory):负责实现对象创建逻辑的对象或方法,该逻辑过于复杂,无法由类的构造函数直接完成。它还可以隐藏被实例化的具体类。工厂方法一般可实现为类的静态方法;
- 存储库(repository):用来访问持久化实体的对象,储存库也封装了访问数据库的底层机制;
- 服务(service):实现不属于实体或值对象的业务逻辑对象;
2. 使用聚合模式设计领域模型
传统领域模型缺少每个业务对象的明确边界,DDD聚合旨在解决此问题。
2.1 聚合拥有明确的边界
聚合模式:将领域模型组织为聚合的集合,每个聚合都是可以作为一个单元进行处理的一组对象构成的图。
Order 聚合及其边界:
- 聚合代表了一致的边界;
- 更新整个聚合可以解决一致性问题;
- 识别聚合是关键;
- 在驱动领域设计中,设计领域模型的关键部分是识别聚合,以及它们的边界和根;
2.2 聚合规则
聚合规则可以确保聚合是一个可以强制执行各种不变量约束的自包含单元。
- 规则一:只引用聚合根;
- 聚合根是聚合中唯一可以由外部类引用的部分;客户端只能通过调用聚合根上的方法来更新聚合;
- 如:服务使用储存库从数据库加载聚合并获取聚合根的引用;
- 规则二:聚合间的引用必须使用主键;
- 如:Order使用consumerId引用其Consumer而不是直接引用Consumer对象;
- 规则三:在一个事务中,只能创建或更新一个聚合;
- 这个约束可以确保单个事务的范围不超越服务的边界;还满足大多数NoSQL数据库的受限事务模型;
- 这个规则让创建或更新多个聚合的操作变得更加复杂,但可以通过Saga解决;
2.3 聚合的颗粒度
- 由于每个聚合的更新都是序列化的,因此更细颗粒度的聚合间提高应用程序能同时处理的请求数量,从而提高可扩展性;
- 另一方面,因为聚合是事务的范围,所以可能需要定义更大的聚合以使特定的聚合更新操作满足事务的原子性;
- 因此,在开发领域模型时,必须做出的关键决策是决定每个聚合的大小;
2.4 使用聚合设计业务
- 在典型的微服务中,大部分业务逻辑由聚合组成;其余业务逻辑存在与领域服务和Saga中;
- Saga编排本地事务的序列,以确保数据的一致性;
- 服务是业务逻辑的入口,由入站适配器调用;
- 服务使用存储库从数据库中检索聚合或将聚合保存到数据库;
- 每个存储库都由访问数据库的出站适配器实现;
2.5 Order Service基于聚合设计的业务逻辑
- 业务逻辑由Order聚合、OrderService服务类、OrderRepository和一个或多个Saga组成;
- OrderService调用OrderRepository来保存和加载Order;
- 对于能在服务内部完成处理的简单请求,服务直接更新Order聚合;
- 如果更新请求跨越多个服务,OrderService将创建一个Saga;
3. 发布领域事件
领域事件:聚合在被创建时,或发生其他重大更改时发布领域事件。
3.1 领域事件的应用场景
- 使用基于编排的Saga维护服务之间的数据一致性【第四章】;
- 通知维护数据副本的服务,源数据已经发生了更改;这种方法称为命令查询职责隔离(CQRS)【第七章】;
- 通过Webhook或消息代理通知不同的应用程序,以触发下一个业务流程;
- 按顺序通知同一应用程序的不同组件;
- 向用户发送短信或电子邮件通知,告诉他们订单发货、航班延误等消息;
- 监控领域事件以验证应用程序是否正常运行;
- 分析领域事件,为用户行为建模;
3.2 领域事件的特点
- 命名领域事件时,往往选择动词的过去分词;
- 领域事件的每个属性都是原始值或值对象;
- 如:OrderCreated事件类具有orderId属性;
- 领域事件通常具有元数据,如事件ID和时间戳;
- 元数据可以是事件对象的一部分,可能在超类中定义;
OrderCreated事件是领域事件的一个例子:
DomainEvent
接口是一个标识接口,用于将类标识为领域事件;OrderDomainEvent
是Order聚合发布的事件的标识接口(如OrderCreated);DomainEventEnvelope
是一个包含事件元数据和事件对象的类;
3.3 事件增强
当OrderCreated的事件接收方需要订单的详细信息时,一种办法是从Order Service中检索该信息,让事件接收方查询聚合服务,缺点是会产生服务请求的开销;
另一种方案是事件增强:
- 事件包含接收方需要的信息;
- 缺点:可能会使领域事件的稳定性降低;每当接收方的需求发生改变时,事件类都可能需要更改;可能会降低可维护性;
3.4 识别领域事件
可以使用事件风暴方法,其结果是一个以事件为中心的领域模型,它由聚合和事件组成;包括以下三个步骤:
- 头脑风暴:请求领域专家集体讨论领域事件;
- 识别事件触发器:请求领域专家确定每个事件的触发器(如:用户操作、外部系统、另一个领域事件、时间的流逝等);
- 识别聚合:请求领域专家识别那些使用命令的聚合并发出相应的事件;
3.5 生成领域事件
在聚合和调用它的服务(或类)之间分配职责。
- 服务可以使用依赖注入来获取对消息传递API的引用,从而轻松发布事件;
- 只要状态发生变化,聚合就会生成事件并将它们返回给服务;
- 聚合可以通过以下方法将事件返回给服务:
在聚合方法的返回值中包括一个事件列表:
该服务调用聚合根方法,然后发布事件;
聚合根在一个内部字段中累积保存事件:然后服务检索这些事件并发布它们;
3.6 发布领域事件
服务必须使用事务性消息来发布事件,以确保领域事件是作为更新数据库中聚合的事务的一部分对外发布;
Eventuate Tram框架提供DomainEventPublisher接口:
让服务实现AbstractAggregateDomainEventPublisher的子类:它为发布领域事件提供了类型安全的接口;
3.7 消费领域事件
领域事件是接收方使用更高级的API,如:Eventuate Tram框架的DomainEventDispatcher等。其可以将领域事件调度到适当的处理程序方法。
- 每当餐馆的菜单更新时,KitchenServiceEventConsumer都会订阅Restaurant Service发布事件;
- 它负责使Kitchen Service的数据副本保持最新;
4. Kichen Service的业务逻辑
该服务的主要功能是负责实现餐馆的订单管理功能。其两个主要聚合是Restaurant和Ticket;
4.1 Kichen Service的设计
- 两个聚合:
- Restaurant:知道餐馆的菜单和营业事件,并可以验证订单;
- Ticket:工单烹饪完成后由送餐员负责派送;
- 核心业务:
- KitchenService:业务入口,定义了创建和更新Restaurant及Ticket聚合的方法;
- TicketRepository:定义了持久化Tickets的方法;
- RestaurantRepository:定义了持久化Restaurants的方法;
- 三个入站适配器:
- REST API:餐馆工作人员通过他们的用户界面调用这些REST API;
- KitchenServiceCommandHandler:由Saga调用的基于异步请求 / 响应的API;它调用KitchenService来创建和更新Ticket;
- KitchenServiceEventConsumer:订阅Restaurant Service发布的事件;它调用KitchenService来创建和更新Restaurant聚合;
- 两个出站适配器:
- DB Adapter:实现TicketRepository和RestaurantRepository接口并访问数据库;
- DomainEventPublishingAdapter:实现DomainEventPublisher接口并发布Ticket领域事件;
4.2 Ticket类的结构
该类使用JPA进行持久化,并映射到TICKETS表。
4.3 Ticket聚合的行为
- create():创建Ticket的工厂方法;
- accept():餐馆已接收订单;
- preparing():餐馆已开始准备订单,意味着订单无法再更改或取消;
- readyForPickup():订单可以派送;
4.4 KitchenService的领域服务
KitchenService由服务入站适配器调用;定义了用于更改订单状态的各种方法(如accept()、reject()、preparing()等);每个方法加载指定的聚合,在聚合根上调用相应的方法,并发布领域事件;如下accept()方法所示:
accept()方法的两个参数:
- orderId:要接受订单的ID;
- readyBy:订单可被派送的预计时间;
4.5 KitchenServiceCommandHandler类
KitchenServiceCommandHandler类是一个适配器,负责处理Order Service实现的各种Saga发送的命令式消息;
5. Order Service的业务逻辑
5.1 Order Service的设计
- 几个入站适配器:
- REST API:供消费者利用用户界面调用的REST API;它调用OrderService来创建和更新Order;
- OrderEventConsumer:订阅Restaurant Service发布的活动;它调用OrderService来创建和更新其Restaurant副本;
- OrderCommandHandlers:由Saga调用的基于异步请求 / 相应的API;它调用OrderService来更新Order;
- SagaReplyAdapter:订阅Saga回复通道并调用Saga;
- 一些出站适配器:
- DB Adapter:实现OrderRepository接口并访问Order Service的数据库;
- DomainEventPublishingAdapter:实现DomainEventPublisher接口并发布Order领域事件;
- OutboundCommandMessageAdapter:实现CommandPublisher接口并向Saga参与方发送命令式消息;
5.2 Order聚合的结构
Order类是Order聚合的根;Order聚合还包括了值对象,如:OrderLineItem、DeliveryInfo和PaymentInfo。
Order类和它的字段:
此类使用JPA持久化,并映射到ORDERS表。
5.3 Order聚合状态机
为了创建或更新订单,Order Service必须使用Saga与其他服务协作。
创建Order过程中调用的Order方法:
更新Order需要调用的方法:
5.4 OrderService类
该类定义了用于创建和更新Orders的方法。
6. 微服务与单体应用程序的业务逻辑异同点
- 相同点:
- 由诸如服务、JPA支持的实体类和存储库等这样的类组成;
- 不同点:
- 领域模型被组织为一组DDD聚合,在其上可以施加各种约束;
- 与传统的对象模型不同,不同聚合中的类之间的引用是基于主键而不是对象引用;
- 事务只能创建或更新单个聚合;聚合在状态发生变化时会发布领域事件;
- 服务通常使用Saga来维护多个服务之间的数据一致性;
7. 本章小结
- 事务脚本模式通常是实现简单业务的好办法。但在实现复杂的业务逻辑时,应该考虑使用面向对象的领域模型模式;
- 设计服务的业务逻辑的好办法是使用DDD聚合。DDD聚合很有用,因为它们把领域模型模块化,消除了服务之间对象的直接引用,并确保每个ACID事务都在服务内;
- 创建或更新聚合时应发布领域事件。领域事件具有广泛的用途。第4章讨论了如何实现协同式Saga。第7章中将讨论如何使用领域事件来更新从其他服务复制来的数据。领域事件的订阅者还可以通知用户和其他应用程序,并将WebSocket消息发布到用户的浏览器。
最后
![](https://img-blog.csdnimg.cn/2021061321525793.jpg)