第7章 在微服务架构中实现查询
前言
在微服务架构中编写查询具有挑战性。查询通常需要检索分散在多个服务所拥有的数据库中的数据,使用传统的分布式查询处理机制虽然在技术上可行,但会打破服务之间的隔离与封装;
在微服务架构中实现查询操作有两种不同的模式:
- API组合模式:这是最简单的方法,应尽可能使用。它的工作原理是让拥有数据的服务的客户端负责调用服务,并组合服务返回的查询结果;
- 命令查询职责隔离(CQRS)模式:它比API组合模式更强大,但也更复杂。它维护一个或多个视图数据库,其唯一的目的是支持查询;
这是一本关于微服务架构设计方面的书,这是本人阅读的学习笔记。下面对一些符号做些说明:
()为补充,一般是书本里的内容;
[]符号为笔者笔注;
1. 使用API组合模式进行查询
1.1 findOrder()查询操作
findOrder()查询操作是从多个服务获取数据的查询方法;
图解:基于微服务架构的FTGO应用程序版本中,数据分散在以下服务中;
- Order Service:基本订单信息,包括详细信息和状态;
- Kitchen Service:从餐馆的角度看订单的状态以及预计取餐时间;
- Delivery Service:订单的交付状态、预计送餐时间及送餐员的当前位置;
- Accounting Service:订单的付款状态;
1.2 什么是API组合模式
图解:
- API组合模式:通过查询每个服务的API并组合结果,实现从多个服务检索数据的查询;
- 其有两种类型的参与者:
- API组合器:它通过查询数据提供方的服务来实现查询操作;
- 数据提供方服务:拥有查询返回的部分数据的服务;
- 是否可以使用此模式实现特定查询操作取决于几个因素,包括数据的分区方式、拥有数据的服务公开的API的功能,以及服务使用数据库的功能;
1.3 使用API组合模式实现findOrder()查询操作
图解:
- 四个提供方服务实现一个REST接口,该接口返回对应于单个聚合的响应;
1.4 设计问题一:由谁来担任API组合器的角色
由服务的客户端:
- 缺点:对于防火墙之外的客户以及通过较慢网络访问的服务,此选择不可用(详情查看第八章);
由实现应用程序外部API的API Gateway:
- 可以使得在防火墙外运行的客户端能够通过单个API调用有效地从众多服务中检索数据;
将API组合器实现为独立的服务
- 在外部访问查询时,由于其聚合逻辑过于复杂,因此无法在API Gateway中完成查询,必须使用单独的服务;
1.5 设计问题二:如何编写有效的聚合逻辑
- 应该使用响应式编程模型;
- 有时API聚合器需要一个提供方服务的结果才能调用另一个服务;在这种情况下,它需要按顺序调用一部分提供方服务;
- 应该使用基于Java CompletableFuture、RxJava可观测或其他类似的响应式设计(详情见《第8章 API Gateway模式》);
1.6 API组合模式的好处与弊端
好处:
- 实现查询操作简单直观;
弊端:
- 增加了额外的开销:涉及多个请求和数据库查询,需要更多的计算和网络资源;
- 带来可用性降低的风险:操作的可用性随着所涉及的服务数量而下降;
- 解决办法:在提供方不可用时,返回先前缓存的数据;或者让API组合器返回不完整的数据;
- 缺乏事务数据一致性;
2. 使用CQRS模式
2.1 为什么要使用CQRS
- 涉及多个服务的查询,API组合模式无法有效实现;
- 因为并非所有服务都存储用于过滤或排序的属性,如Order Service和Kitchen Service两项服务存储了Order的菜单项,而Delivery Service和Accounting Service都不存储菜单项;
- 解决办法:让API组合器进行内存中连接;让API组合器从Order Service和Kitchen Service检索匹配的订单,然后通过ID从其他服务请求订单;
- CQRS可以解决服务的数据库(数据模型)不能有效查询的问题;
- 如:在进行地理空间查询时会生成数据副本,CQRS解决了同步副本问题;
- CQRS考虑隔离问题的必要性,避免过多的职责导致过载服务;
2.2 CQRS隔离命令与查询
图解:
- 位于命令端的领域模型处理CRUD操作并映射到其自己的数据库;
- 命令端在数据发生变化时发布领域事件;
2.3 CQRS和查询专用服务
图解:
- 查询服务的API只包含查询操作,并无命令操作;
- 它通过订阅一个或多个其他服务发布的事件来确保它的数据库是不断更新的,并由此实现查询操作;
- 查询端服务订阅由多个服务发布的事件;
2.4 CQRS的好处与弊端
好处:
- 在微服务架构中高效地实现查询;
- 高效地实现多种不同的查询类型;
- 在基于事件溯源技术的应用程序中实现查询;
- 更进一步地实现问题隔离;
弊端:
- 更加复杂的架构:开发人员必须编写更新和查询视图的查询端服务;
- 处理数据复制导致的延迟;
- 即:更新聚合后查询聚合会看到聚合的先前版本;
- 解决方案:采用命令端和查询端API为客户端提供版本信息,使其能够判断查询端是否过时;
3. 设计CQRS视图
CQRS视图模块包括由一个或多个查询操作组成的API;
3.1 选择视图存储库
NoSQL:
- CQRS视图受益于NoSQL数据库更丰富的数据模型和性能,不受NoSQL数据库事务处理能力的限制;
SQL数据库:
- 在主流硬件上运行的现代关系型数据库具有出色的性能;
- SQL数据库通常具有非关系特征的扩展,如地理空间数据类型和查询;
- CQRS视图可能需要使用SQL数据库才能支持报表引擎;
支持更新操作:
- 通常使用其主键更新或删除视图数据库中的记录;
- 有时需要使用类似外键的做法来更新或删除记录;
3.2 设计数据访问模块
事件处理程序和查询API模块不直接访问数据库存储区。相反,它们使用数据访问模块,该模块由数据访问对象(DAO)及其辅助类组成;
- 处理并发更新确保更新幂等;
- 当视图订阅由多个聚合类型发布的事件时,多个事件处理程序可能同时更新同一记录;
- 幂等事件处理程序:
- 为了确保可靠,事件处理程序必须记录事件ID并以原子化的方式更新数据存储区,如何试下取决于数据库类型;
- 事件处理程序不需要记录每个事件的ID,每个记录仅需要存储从给定聚合实例接收的max(eventId);
- 让客户端应用程序采用最终一致性的视图:
- 执行更新命令后执行查询命令可能看到的是更新前的数据 [有延迟],客户端可以使用以下方法检测这种不一致性:
- 命令端操作将包含已发布事件和ID标记返回给客户端。然后,客户端将事件有关的ID传递给查询操作,如果该事件尚未更新视图,则返回查询错误;
- 视图模块可以使用重复事件检测机制来实现这样的功能;
3.3 添加和更新CQRS视图
- 添加和更新CQRS视图在概念上很简单,即:
- 创建新视图:开发查询端模块、设置数据存储区并部署服务。查询端模块的事件处理程序处理所有事件,最终视图将是最新的;
- 更新现有视图:更改事件处理程序并从头开始重构视图;
- 但在实际中会产生一些问题,如下:
- 消息代理无法无限期存储信息;
- 如:RabbitMQ会在消费者处理完消息后删除该消息;Apache Kafka可在配置的保留期内保留消息,但也不会无限期存储事件;
- 解决办法:使用归档事件构建CQRS视图,使用可扩展的大数据技术(如Apache Spark)实现;
- 处理所有事件所需的时间和资源随时间推移而不断增长;
- 解决办法:增量式构建CQRS视图,使用两步增量算法。第一步基于先前的快照和自创建快照以来发生的事件,定期计算每个聚合实例的快照;第二步使用快照和任何后续事件创建视图;
4. 实现基于AWS DynamoDB的CQRS视图
介绍如何使用DynamoDB为findOrderHistory()操作实现CQRS视图;
4.1 OrderHistoryService的设计
图解:
- OrderHistoryEventHandlers:订阅各种服务发布的事件并调用OrderHistoryDAO;
- OrderHistoryQueryAPI模块:实现REST API接口;
- OrderHistoryDataAccess:包含OrderHistoryDAO,它定义了更新和查询ftgo-order-history DynamoDB表及其辅助类的方法;
- ftgo-order-history DynamoDB表:存储订单的DynamoDB表;
4.2 OrderHistoryEventHandlers模块
此模块由接收事件和更新DynamoDB表的事件处理程序组成;
图解:
- 每个事件处理程序都有一个DomainEventEnvelope类型的参数,其中包含事件和描述事件的一些元数据;
5. 本章小结
- 实现从多个服务检索数据的查询具有挑战性,因为每个服务的数据都是私有的;
- 有两种方法可以实现这些类型的查询:API组合模式和命令查询职责隔离(CQRS)模式;
- 从多个服务获取数据的API组合模式是实现查询的最简单方法,应尽可能使用;
- API组合模式的局限性是某些复杂查询需要大型数据集的低效内存连接;
- 使用视图数据库实现查询的CQRS模式功能更强大,但实现起来更复杂;
- CQRS视图模块必须处理并发更新以及检测和丢弃重复事件;
- CQRS有助于改善问题隔离,服务不必为自己拥有的数据实现查询功能;
- 客户必须处理CQRS视图的最终一致性;
最后
![](https://i-blog.csdnimg.cn/blog_migrate/4c125f9add4205c2c8b94d529690a7c5.jpeg)