在日常工作中,你是否也遇到过下面几种情况:
为什么会出现这种现象?其本质仍旧是代码组织结构不合理,我们将不同的复杂性揉在一起,从而造成了更大的复杂性,然后如此往复,不知不觉中陷入巨大的复杂性旋涡不可自拔。
1. CQRS 是什么?
CQRS 是 Command Query Responsibility Segregation 得简称,简单理解就是对 “写”(Command) 和 “读” (Query)操作进行分离。反应快的同学会说:“也不是什么高深技术吗,不就是数据库的读写分离吗?”
是的,数据库的读写分离也算是一种 CQRS,但 CQRS 的含义要比这复杂的多。
CRQS 既是一种流行的业务架构,又是一种设计思维。
CQRS 的核心是“拆分”,将复杂系统拆分为 Command 和 Query 两个部分,针对不同的场景使用不同的模式,选择最合适的技术落地最佳解决方案,避免两者相互掣肘相互影响。
CQRS的目的是降低整个系统的复杂性,那它背后的逻辑是什么?
假设,在一个系统中:
如果使用同一套模型来处理 Command 和 Query,那在极端情况下,系统的复杂性为 M * N,因为两者相互影响,调整一方的同时要时刻关注对另一方的影响。
图片
这种“你中有我,我中有你”的设计方式,“两者的相互影响”成为系统最为复杂之处,大量精力消耗在“排查影响”,而非最有价值的设计和编码。
如果,将 Command 和 Query 彻底分离,系统的复杂性变成 M + N。Command 的变更不会影响 Query,而 Query 的修改也不会影响 Command。
图片
当然,以上两个极端在实际工作中也很少见,通常系统的复杂性介于两者之间。
图片
这只是从理论进行推导,在实际工作中随处可见的“冲突”也是对“拆分”的一种暗示。
2. 分层架构中的冲突
以最常见的分层架构进行介绍,具体如下:
图片
如图所示,将系统分成5层,每层的含义如下:
其实,分层架构本身也是一种“拆分”,将不同的关注点封装在不同的层次。但除了横向分层,还可以基于 CQRS 对其进行纵向拆分,也就是将每个层的组件拆分为 Command 和 Query 两部分。
由于接入层冲突较小,本身拆分的意义不大,在此不做要求,但从严格意义上讲,仍旧建议进行拆分。
3. 应用服务层冲突与拆分
应用服务层拆分就是将一个应用服务拆分为 CommandService 和 QueryService 两组。
图片
这样做可以避免很多不必要的麻烦,Command 和 Query 存在较大的区别,具体如下:
CommandService |
QueryService |
|
依赖组件不同 |
ValidateService 验证服务;LazyLoaderFactory 延迟加载服务;CommandRepository 不带缓存的仓库;EventPublisher 事件发表器 |
QueryRepository 带缓存功能的仓库;JoinService 数据聚合服务;Converter 数据转换服务 |
核心流程不同 |
验证、加载、业务操作、同步、发布事件 |
验证、加载、数据组装、转换 |
功能加强不同 |
主要是事务管理器 |
主要是缓存组件 |
回想开篇时提到的场景,完成应用层拆分,就不在为使用错组件而烦恼:
除此之外,针对统一的操作流程,还可以进一步抽象来消除重复的“模板代码”,比如:
抽象出 BaseCommandService 和 BaseQueryService 两个父类用于统一核心流程
子类实现 BaseCommandService 和 BaseQueryService 的抽象方法完成功能扩展
按规范定义 CommandService 和 QueryService 接口,通过注解完成相关配置
自动生成 Proxy 实现类,完成流程编排
4. 模型层冲突与拆分
模型层是系统的核心,它的设计直接影响整个系统的质量。作为承接业务逻辑的核心,比较流程的实现策略包括:
关于哪个才是最优解,网上已经争论多年,最终也没有结论。但我始终认为“没有业务场景就讨论方案,就是在耍流氓”。
从不同应用场景出发便可得到如下结论:
我经常说:“最简单的“写”也是复杂,最复杂的“读”也是简单”,其背后逻辑是基于对 Command 和 Query 的场景判断。
将模型拆分为 Command 和 Query,具体如下:
图片
完成模型拆分后,新模型具有以下特征:
这块是拆分的重点,为了方便理解,简单举个例子:
比如在电商的订单模块:
- 生单流程,由 Order 作为聚合根对内部 OrderItem 和 PayInfo 进行统一协调
- 订单列表页,只需展示 Order 和 User 信息
- 订单详情,需要展示Order、User、Address、OrderItem、PayInfo、Product等信息
如果让一个模型同时支持着三个场景,那模型自己就变的非常复杂,很难判断某个方法、某个字段究竟属于哪个场景。
此时,应该根据场景对模型进行拆分:
三个模型相互独立,互不影响。
当然,由于使用统一的 Repository 还需提供对应 VO 的 Converter:
5. 仓库层冲突与拆分
仓库层拆分也是非常有必要的,在这一层主要有几项冲突:
CommandRepository |
QueryRepository |
|
底层实现不同 |
主要基于 DB 实现 |
基于 DB、Redis、ES 等多种存储引擎 |
方法复杂性不同 |
提供仅有的少量方法并足以支持大多数场景,比如 save、update、getById 等 |
根据业务场景进行定制,方法多种多样(单条、批量、分页、排序、统计等),维度多种多样(id、user、status) |
返回值不同 |
直接返回装配完整的富对象 |
根据业务场景定制返回值 |
仓库拆分后整体架构如下:
图片
仓库拆分具有以下特点:
6. 数据层冲突与拆分
数据层拆分是最重要的拆分,提到分离第一反应也是“数据库主从分离”。
数据层拆分的本质是:各种数据存储引擎的最佳应用场景相差巨大,读 和 写 优化往往存在矛盾。
仍旧以最常见的数据库为例:
鱼和熊掌不可兼得,在数据库层展示的淋漓尽致!
数据层拆分后架构如下:
图片
该模型具有以下特点:
数据层拆分是大型系统最终的归宿,仍旧以订单系统为例:
这就是我们面临的现状:“数据密集型系统”越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来,通过 API 的方式,对外提供服务,屏蔽内部的复杂性。
7. 小结
“拆分”是“分离关注点”的重要手段之一。拆分的目的是将问题进行归类,然后采取有针对性的手段更好的解决问题。
CQRS 作为一种架构,将业务系统不同部分进行归类,接下来需要为 Command 和 Query 寻找最优解决方案:
聚合设计
仓库设计
LazyLoad + Context 模式
业务验证
领域事件
…
QueryObject 查询对象模式
内存 Join 模式
宽表&冗余表模式