微服务架构设计模式-第五章
第5章 微服务架构中的业务逻辑设计
由于业务逻辑散步在多个服务上,因此在微服务架构中开发复杂的业务逻辑更具挑战性。我们需要解决两个关键问题。
首先,典型的领域模型是由各种类(class)交织在一起的一个网络。虽然这在单体应用程序中不是问题,但在微服务架构中,类分散在不同的服务中,你需要避免跨越服务边界(也就是进程)的对象引用。
第二个挑战是设计在微服务架构下的逻辑,这些业务逻辑受到微服务下事务管理的各种约束。必须使用saga模式来维护服务之间的数据一致性。
幸运的是,可以使用领域驱动设计中的聚合模式(aggregate)来解决这些问题。聚合模式下下,服务的业务逻辑提供多个聚合组成的一个集合来体现。聚合时一组对象,可以作为一个单元来处理。在微服务架构中开发业务逻辑时,聚合可以起到两个重要的作用:
- 使用聚合可以避免任何跨服务边界的对象引用,因为聚合之间通过主键进行引用,而不是通过对象的地址进行引用
- 由于单个事务只能创建或更新单个集合,因此聚合满足微服务事务模型的约束。
因此,我们可以确保单个服务中的事务都满足ACID特效。
5.1 业务逻辑组织模式
5.1.1 使用事务脚本模式设计业务逻辑

在开发简单的业务逻辑时,更好的方法是编写面向过程的代码,并使用martin fowler《patterns of enterprise application architecture》一书中提到的事务脚本模式。可以编写一个成为 事务脚本 的方法来处理来自表示层的每个请求,而不是进行任何面向对象的设计。这种方法的一个重要特征是实现行为的类与存储状态的类是分开的。
《企业应用架构模式》 ISBN:9787 1113 03930
使用事务脚本模式时,脚本通常位于服务类中,在此示例中是OrderService类。每个服务类都有一个用于请求或系统操作的方法。这个方法实现该请求的业务逻辑。它使用数据访问对象(DAO)访问数据库,例如OrderDao。数据对象是纯数据,几乎没有行为。
5.1.2 使用领域模型模式设计业务逻辑

在这个例子中,Order类具有状态和行为。此外,它的状态是是有的,只能通过它的方法间接访问。
5.1.3 关于领域驱动设计
Eric Evans《Domain Driven Design》
领域驱动设计——软件核心复杂性应对之道
- 实体(entity):具有持久化ID的对象
- 值对象(value object):作为值集合的对象。具有相同属性值的两个值对象可以互换使用。例子:Money类,由币种和金额组成。
- 工厂(factory):负责实现对象逻辑的对象或方法,该逻辑过于复杂,无法由类的构造函数直接完成。它还可以隐藏被实例化的具体类。工厂方法一般可实现为类的静态方法
- 存储库(repository):用来访问持久化实体的对象,存储库也封装了访问数据库的底层机制
- 服务(service):实现不属于实体或值对象的业务逻辑的对象
5.2 使用聚合模式设计领域模型

这种传统领域模型缺少每个业务对象的明确边界。例如,它没有指定哪些类是Order业务对象的一部分。
5.2.1 模糊边界所带来的问题
订单中的order不仅仅是order对象,还有订单行项目、付款信息等。
5.2.2 聚合拥有明确的边界

聚合是一个边界内的领域对象的集群,可以将其视为一个单元。它由根实体和可能的一个或多个其他实体和值对象组成。许多业务对象都被建模为聚合。
聚合将领域模型分解为块,单独的每一块更容易理解。它们还阐明了加载、更新和删除等操作的范围。这些操作作用于整个聚合而不是部分聚合。聚合通常从数据库中完整加载,从而避免了延迟加载所导致的任何复杂性。删除聚合会从数据库中删除其所有对象。
聚合代表了一致的边界
识别聚合时关键
5.2.3 聚合的规则
规则一:只引用聚合根
要求聚合根是聚合中唯一可以由外部类引用的部分。客户端只能通过调用聚合根上的方法来更新聚合。
规则二:聚合间的引用必须使用主键
order 使用 consumerId 引用 consumer,而不是直接引用 consumer 对象。
聚合同时也是存储的单元,这种方法让持久化变得简单。我们可以更容易将聚合存储在nosql数据库。通过主键引用聚合,因此不再需要透明延迟加载(transparent lazy loading),同时也避免了它所带来的问题。通过分片(sharding)聚合来横向扩展数据库也相对简单。
一句话核心:延迟加载这件事,对上层调用代码完全透明、无感,不用改业务代码;只有第一次真正访问属性 / 对象时,才去加载真实数据 / 创建重型对象
重点区分:延迟加载 = 按需加载;透明 = 使用者感知不到代理的存在,写法和直接用原生对象一模一样
规则三:一个事务中,只能创建或更新一个聚合
确保单个事务的范围不超越服务的边界。此约束还满足大多数NoSQL数据库的受限事务模型。
这个规则让创建或更新多个聚合的操作变得更加复杂。但这正是saga旨在解决的问题。saga 的每一步都只创建或更新一个集合。

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

业务逻辑由 order 聚合、orderService服务类、orderRepository 和一个或多个 saga 组成。orderService 调用 orderRepository 来保存和加载 order。对于能在服务内部完成处理的简单请求,服务直接更新 order 聚合。如果更新请求跨越多个服务, orderService 将创建一个 saga
5.3 发布领域事件
在领域驱动设计的上下文中,领域事件是聚合发生的事情。它由领域模型中的一个类表示。事件通常代表状态的变化。order 聚合可以在每次继续状态变化时发布一个或多个事件,给那些感兴趣的接收方。
5.3.1 为什么需要发布变更事件
应用程序的其他协作方通常有兴趣了解聚合的状态更改。以下是一些可能的场景。
- 使用基于编排的 saga 维护服务之间的数据一致性
- 通知维护数据副本的服务,源数据已经发生了更改。这种方法称为命令查询职责隔离(CQRS)
- 通过 webhook 或消息代理通知不同的应用程序,以触发下一步业务流程
- 按顺序通知同一应用程序的不同组件,丽日,将 webSocket 消息发送到用户的浏览器或更新如 elasticSearch 这样的文本数据库
- 向用户发送短信或电子邮件通知,告诉他们订单已发货、他们的医疗处方已经准备就绪,或者他们的航班延误
- 监控领域事件以验证应用程序是否正常运行
- 分析领域事件,为用户行为建模
5.3.2 什么是领域事件
在命名领域事件时,我们往往选择动词的过去分词。这样的命名能够明确表达事件的一些属性。领域事件的每个数学都是原始值或值对象。例如,orderCreated 事件类具有 orderId 属性。
领域事件通常还具有元数据,例如事件ID和时间戳。它也可能包含执行力此次更改的用户的身份,因为这对用户行为审计很有用。元数据可以是事件对象的一部分,可能在超类中定义。事件元数据也可以位于封装事件对象的 "信封对象" 中。触发事件的聚合的ID也可以是事件 "信封对象" 的一部分,而不是明确的事件属性。
5.3.3 事件增强
假设在处理 OrderCreated 事件时。事件接收方可能需要订单的详细信息。一种选择是从 Order Service 中检索该信息,让事件接收方查询聚合服务,但这样做的缺点是它会产生服务请求的开销。
另一种称为事件增强的方法是,事件包含接收方需要的信息。它简化了事件接收方,因为他们不再需要从发布事件的服务请求数据。
class OrderCreated implements OrderEvent {
private List<OrderLineItem> lineItems;
private DeliveryInformation deliveryInformation;
private long restaurantId;
private String restaurantName;
...
}
缺点:使领域事件的稳定性降低。每当接收方的需求发生变化时,事件类都可能需要更改。可能会降低可维护性,因为这种更改会影响应用程序的多个部分。
5.3.4 识别领域事件
需求描述比如 “当 X 发生时做 Y ”
事件风暴
- 头脑风暴,请领域专家集体讨论领域事件
- 识别事件触发器:请求领域专家确定每个事件的触发器
- 用户操作
- 外部系统
- 另一个领域事件
- 事件的流逝
- 识别聚合
5.3.5 生成和发布领域事件
生成领域事件
聚合可以直接调用消息传递API,弊端在于聚合不能使用依赖注入,所以消息传递API需要作为方法参数传递。这将把基础设施和业务逻辑交织在一起,是非常不可取的。
在聚合和调用它的服务(或类)之间分配职责。服务可以使用依赖注入来获取对消息传递API的引用,从而轻松发布事件。只要状态发生变化,聚合就会生成事件并将它们返回给服务。
1)在聚合方法的返回值中包括一个事件列表

方法简单,唯一的缺点是非 void 方法的返回类型会变得更复杂。它们必须同时返回包含原始返回值和Litst
2)聚合根在一个内部字段中累计保存事件,然后,服务检索这些事件并发布它们。

弊端:为了减少代码重复,聚合根应该扩展一个超类,,这可能与扩展其他一些超类的要求相冲突。另一个问题是虽然聚合根的方法很容易调用 registerDomainEvent(),但聚合中其他类的方法会发现调用该方法具有挑战性。它们很可能需要以某种方式将事件传递给聚合根。
如何可靠地发布领域事件
服务必须使用事务性消息来发布事件,以确保领域事件是作为更新数据库中聚合的事务的一部分对外发布。
5.3.6 消费领域事件
领域事件最终作为消息发布到消息代理,例如Apache Kafka。领域事件的接收方可以直接使用事件代理的客户端API。但是使用更高级的API比较方便,例如第3章中描述的Eventuate Tram 框 的 DomainEventDispatcher.
5.4 Kitchen Service的业务逻辑
略
5.5 Order Service 的业务逻辑
略
5.5.1 Order聚合
略
5.5.2 OrderService类
略

浙公网安备 33010602011771号