第5章 微服务架构中的业务逻辑设计
图5-1显示了一个典型的服务架构。业务逻辑是六边形架构的核心。业务逻辑的周围是入站和出站适配器。入站适配器处理来自客户端的请求并调用业务逻辑。出站适配器被业务逻辑调用,然后在调用其他服务和外部应用程序。
业务逻辑通常是服务中最复杂的部分。在开发业务逻辑时必须做出的关键决策是选用面向对象的方式,还是面向过程的方式。组织业务逻辑主要有两种模式:面向过程的事务脚本模式和面向对象的领域建模模式。
5.1.1 使用事务脚本设计业务逻辑
事务脚本的一个重要特征是实现行为的类与存储状态的类是分开的。
使用事务脚本模式时,脚本通常位于服务类中,在此实例中是OrderService类。每个服务类都有一个用于请求或系统操作的方法。这个方法实现该请求的业务逻辑。它使用数据访问对象(DAO)访问数据库。数据对象(Order)是纯数据,几乎没有行为。
5.1.2 使用领域模型模式设计业务逻辑
实际上,就像单体应用程序不断增长的趋势一样,事务脚本也存在同样的问题。因此,除非是编写一个非常简单的应用程序,否则你应该抵制编写面向过程的代码的诱惑,使用领域模型模式,并进行面向对象的设计。
在面向对象的设计中,业务逻辑由对象模型和相对较小的一些类的网络组成。这些类通常知己诶对应于问题域中的概念。在这样的设计中,有些类只有状态和行为,但很多类同时包含了状态和行为,这样的类都是精心设计的。图5-3展示了领域模型模式的示例。
使用面向对象设计有许多好处。首先,这样的设计易于理解和维护。它不是由一个完成所有事情的大类组成,而是由许多小类组成,每个小类都有少量的职责。其次,我们的面向对象设计更容易测试:每个类都可以并且应该能够被独立测试。最后,面向对象的设计更容易扩展,因为它可以使用众所周知的设计模式。
5.1.3 关于领域驱动设计
5.2 使用聚合模式设计领域模型
5.2.1 模糊编辑所带来的问题
假设你想要哦在Order业务对象上执行操作,例如加载或删除。这到底是什么意思呢?操作范围是什么?当然你会加载或删除Order对象。但实际上,订单中的Order不仅仅是Order对象。还有订单行项目、付款信息等。
除了概念模糊之外,缺少明确的边界会在更新业务对象时导致问题。典型的业务对象有一些不变量,即必须始终强制执行的业务规则。例如,Order具有最小订单金额。
5.2.2 聚合拥有明确的边界
聚合是一个边界内的领域对象的集群,可以将其视为一个单元。它由根实体和可能的一个或多个其他实体和值对象组成。
图5-5显示了Order聚合及其边界。Order聚合有Order实体、一个或多个OrderLineItem值对象以及其他值对象组成。
聚合将领域模型分解为块,单独的每一块更容易理解。它们还阐明了加载、更新和删除等操作的范围。这些操作作用于整个聚合而不是部分聚合。聚合通常从数据库中完整加载,从而避免了延迟加载所导致的任何复杂性问题。删除聚合会从数据库中删除其所有对象。
聚合代表了一致的边界
更新整个聚合而不是聚合的一部分,在聚合跟上调用更新操作,这会强制执行各种不变量的约束。此外,可以使用例如版本号或数据库级锁锁定聚合根来处理并发性。
识别聚合根是关键
在领域驱动设计中,设计领域模型的关键部分是识别聚合,以及它们的边界和根。聚合内部结构的细节是次要的。
5.2.3 聚合的规则
规则一:只引用聚合根
聚合根是唯一可以由外部类引用的部分。客户端只能通过调用聚合根上的方法来更新聚合。
规则二:聚合间引用必须使用主键
聚合必须通过标识(例如,主键)而不是对象引用。
这种方法与传统的对象建模完全不同,传统的对象建模将领域模型中的外键视为不好的设计。使用标识而不是对象引用意味着聚合是松耦合的。它确保聚合之间的边界得到很好的定义,并避免意外更新不同的聚合。此外,如果聚合是另外一个服务的一部分,则不会出现跨服务的对象引用的问题。
聚合同时也是存储的单元,因此这种方法让持久化也变得简单。我们可以更容易地将聚合存储在NoSQL数据库中。
规则三:在一个事务中,只能创建或更新一个聚合
聚合必须遵守的另一个规则是一个事务只能创建或更新一个聚合。
在单个服务中维护多个聚合的一致性的另外一种方法是打破聚合规则,在一个事务中更新多个聚合。例如,服务B可以在单个事务中更新聚合Y和Z。只有在使用支持复杂事务模型的数据库(如关系数据库)时才能实现此目的。如果你使用的NoSQL数据库只有简单的事务,除了使用Saga外别无选择。
5.2.4 聚合的粒度
在开发领域模型时,你必须做出的关键决策是决定每个聚合的大小。一方面,聚合理想上应该很小。由于每个聚合的更新都是序列化的,因此更细粒度的聚合将提供应用程序能同时更新一个聚合而引发冲突的可能性。但是另一方面,因为聚合是事务的范围,所以你可能需要定义更大的聚合以使特定的聚合更新操作满足事务的原子性。
5.2.5 使用聚合设计业务逻辑
图5-9显示了Order Service基于聚合设计的业务逻辑。
业务逻辑有Order聚合、OrderService服务类、OrderRepository和一个或多个Saga组成。OrderService调用OrderRepository来保存和加载Order。对于能在服务内部完成处理的简单请求,服务直接更新Order聚合。如果更新请求跨越多个服务,OrderService将创建一个Saga。
5.3 发布领域事件
5.3.1 为什么需要发布变更事件
领域事件很有用,因为应用程序的其他协作方(比如客户端、其他应用程序或统一应用程序中的其他组件)通常有兴趣了解聚合状态的更改。以下是一些可能的场景:
- 使用基于编排的Saga维护服务之间的数据一致性
- 通知维护数据服务的服务,源数据已经发生了更改。这种方法称为命令查询职责隔离(CQRS)。
- 通过Webhook或消息代理通知不同的应用程序,以触发下一步业务流程
- 按顺序通知同一应用程序的不同组件
- 向用户发送短信或者电子邮件通知
- 监控领域事件以验证应用程序是否正常运行
- 分析领域事件,为用户行为建模
5.3.2 什么是领域事件
在命令领域事件时,我们往往选择动词的过去分词。领域事件的每个属性都是原始值或值对象。
领域事件通常还具有元数据,例如事件ID和时间戳。它也可能包含执行了此次更改的用户的身份,因为这对用户行为审计很有用。
5.3.3 事件增强
在处理OrderCreated事件时,事件接收方可能需要订单的详细信息。一种选择是从Order Service中检索该信息,让事件接收方查询耦合服务,但这样的缺点是它会产生服务请求的开销。
另一种成为事件增强的方法是,事件包含接收方需要的信息。在OrderCreated事件中,Order聚合可以通过包含订单详细信息来增强事件。
虽然事件增强简化了接收方,但缺点是它可能会使领域事件的稳定性降低。每当接收方的需要发生变化时,事件类都可能需要更改。这可能会降低可维护性,因为这种更改会影响应用程序的多个部分。
5.3.4 识别领域事件
略~
5.3.5 生成和发布领域事件
略~
5.3.6 消费领域事件
略~
5.4 Kitchen Service的业务逻辑
略~
Order Service的业务逻辑
略~
第6章 使用事件溯源开发业务逻辑
6.1 使用事件溯源开发业务逻辑架构
事件溯源是构建业务逻辑和持久化聚合的另一种选择。它将聚合以一系列事件的方式持久化保存。每个事件代表聚合的一次状态变化。应用程序通过重放事件来重新创建聚合的当前状态。
6.1.1 传统持久化技术的问题
对象与关系的“阻抗失调”
所谓的对象与关系的“阻抗失调”是一个古老的问题。关系型数据库的表结构模式,与领域模型及其复杂关系的图状结构之间,存在基本的概念不匹配问题。人们一直就对象与关系映射(ORM)框架是否真正有效这个问题进行辩论,这也是“阻抗失调”问题某些方面的一种体现。
缺乏聚合历史
传统持久化的另一个限制是它只保存聚合的当前状态。聚合更新后,其先前的状态将丢失。如果应用程序必须保留聚合的历史记录(可能是出于监管目的),那么开发人员必须自己实现此机制。
实施审计功能将非常繁琐且容易出错
另一个问题是审计功能。许多应用程序必须维护审计日志,用于跟踪哪些用户更改了聚合。实施审计的挑战在于,除了这是一项耗时的工作之外,负责记录审计日志的代码可能会和业务逻辑代码发生偏离,从而导致各种错误。
事件发布是凌驾于业务逻辑之上
传统持久化的另一个限制是它通常不支持发布领域事件。某些ORM框架可以再数据对象更改时调用应用程序提供的回调接口。但是,我们无法把自动发布消息作为更新数据事务的一部门。开发人员必须自己处理事件生成的逻辑,这可能会与业务逻辑代码不完全同步。
6.1.2 什么是事件溯源
事件溯源是一种以事件为中心的技术,用于数显业务逻辑和聚合的持久化。聚合作为一系列事件存储在数据库中。每个事件代表聚合的状态变化。聚合的业务逻辑围绕生成和使用这些事件的要求而构建。
6.1.3 使用乐观锁处理并发更新
两个或多个请求同时更新统一聚合的情况并不少见。使用传统持久化技术的应用程序通常使用乐观锁来防止一个事务覆盖另一个事务的更改。乐观锁通常使用版本列来检测聚合自读取以来是否已经更改。应用程序将聚合根映射到具有VERSION列的表,每当更新聚合时,该列都会递增。
6.1.4 事件溯源和发布事件
使用轮询发布事件
事件发布方可以通过执行SELECT语句轮询EVENTS表,以查找新事件,并将事件发布到消息代理。这种做法的挑战在于如何确定哪些事件是新事件。例如,假设eventIds是单调递增的。表面上可行的方法是让事件发布方记录它已经处理的最后一个eventId。然后它将使用如下查询检索新事件:
SELECT * FROM EVENTS where event_id > ? ORDER BY event_id ASC
这种方法的问题在于事务可以按照与生成事件不同的顺序提交。因刺激,事件发布方可能会意外跳过事件。图6-6显示了一个场景。
此问题的一个解决方案是向EVENTS表添加一个额外的列,以跟踪事件是否已发布。
使用事务日志拖尾技术来可靠地发布区事件
它保证事件会被发布,并且具备高性能和可扩展性。
6.1.5 使用快照提升性能
长声明周期的聚合可能会产生大量的事件。随着时间的推移,加载和重放这些事件会变得越来越低效。
常见的解决方案是定期持久保存聚合状态的快照。图6-7显示了使用快照的示例。
6.1.6 幂等方式的消息处理
如果可以使用相同的消息多次安全地调用消息接收方,则消息接收方是幂等的。
基于关系型数据库事件存储库的幂等消息处理
如果应用程序使用基于关系型数据库的时间存储库,则它可以使用相同的方法来检测和丢弃重复消息。它将消息ID插入PROCESSED_MESSAGES表,作为插入EVENTS表的事件的事务的一部分。
基于非关系型数据库事件存储的幂等消息处理
基于NoSQL的事件存储库的事务模式往往功能有限,必须使用不同的机制来实现幂等消息处理。消息接收方必须以某种原子化的方式同时完成事件持久化和记录消息ID。
6.1.7 领域事件的演化
事件溯源在概念上将会永久存储事件,而这是一把双刃剑。一方面,它能为应用程序提供准确的更改信息的审计日志。它还使应用程序能够重建聚合的历史状态。另一方面,它也会带来一个挑战,因为事件的结构经常随时间的推移而变化。应用程序可能需要处理多个事件版本。
表6-1显示了可能发生的不同类型的更改。
通过向上转换(Upcasting)来管理结构变化
事件溯源应用程序可以在从事件存储库加载事件时执行转换。通常用成为“向上转换”的组件将各个事件重旧版本更新为更新的版本。因此,应用程序代码只需处理当前事件结构。
6.1.8 事件溯源的好处
- 可靠地发布领域事件
- 保留聚合的历史
- 最大限度地避免对象与关系的“阻抗失调”问题
- 为开发者提供一个“时光机”
6.1.9 事件溯源的弊端
- 有一定的学习曲线
- 基于消息传递的应用程序的复杂性
- 处理事件的演化有一定的难度
- 删除数据存在一定的难度
- 查询时间存储库非常有挑战性
6.2 实现事件存储库
事件存储库是数据库和消息代理功能的组合。它表现为数据库,因为它具有通过主键插入和检索聚合事件的API。它表现为消息代理,因为它有一个用于订阅事件的API。
略~
6.3 同时使用Saga和事件溯源
略~