cqrs是什么意思 DDD与CQRS的组合使用优势

在日常工作中,你是否也遇到过下面几种情况:
使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能抗住!”
开发一个后台管理功能,业务反馈说数据一直不对,对比后发现缓存与数据库不一致,为什么要使用缓存接口呢,你陷入沉思?
产品要求在 xxx 上增加新功能,编码、测试、上线一气呵成,最后发现另外一个流程被躺枪,出现异常不得不进行回滚!
在一个高并发的场景,db 成为了系统瓶颈,不加索引查询扛不住,加索引更新扛不住,又该如何处理?
随着数据量的激增,系统变得越来越慢,特别是后台管理复杂的查询场景下,复杂的 join 让 db 不堪重负
……
为什么会出现这种现象?其本质仍旧是代码组织结构不合理,我们将不同的复杂性揉在一起,从而造成了更大的复杂性,然后如此往复,不知不觉中陷入巨大的复杂性旋涡不可自拔。
1. cqrs 是什么?
cqrs 是 command query responsibility segregation 得简称,简单理解就是对 “写”(command) 和 “读” (query)操作进行分离。反应快的同学会说:“也不是什么高深技术吗,不就是数据库的读写分离吗?”
是的,数据库的读写分离也算是一种 cqrs,但 cqrs 的含义要比这复杂的多。

crqs 既是一种流行的业务架构,又是一种设计思维。

cqrs 的核心是“拆分”,将复杂系统拆分为 command 和 query 两个部分,针对不同的场景使用不同的模式,选择最合适的技术落地最佳解决方案,避免两者相互掣肘相互影响。
cqrs的目的是降低整个系统的复杂性,那它背后的逻辑是什么?
假设,在一个系统中:
command 的复杂性为 m
query 的复杂性为 n
如果使用同一套模型来处理 command 和 query,那在极端情况下,系统的复杂性为 m * n,因为两者相互影响,调整一方的同时要时刻关注对另一方的影响。
这种“你中有我,我中有你”的设计方式,“两者的相互影响”成为系统最为复杂之处,大量精力消耗在“排查影响”,而非最有价值的设计和编码。
如果,将 command 和 query 彻底分离,系统的复杂性变成 m + n。command 的变更不会影响 query,而 query 的修改也不会影响 command。
当然,以上两个极端在实际工作中也很少见,通常系统的复杂性介于两者之间。
这只是从理论进行推导,在实际工作中随处可见的“冲突”也是对“拆分”的一种暗示。
2. 分层架构中的冲突
以最常见的分层架构进行介绍,具体如下:
如图所示,将系统分成5层,每层的含义如下:
「web 接入层。」 主要用于处理系统输入,对输入信息进行验证,调用应用服务完成业务操作,对结果进行转换,最终返回给调用方;
「应用服务层。」 主要处理业务流程编排,从仓库中获取领域对象,执行领域模型的业务操作,将最新的对象状态通过仓库同步到数据存储引擎,并对外发布领域事件;
「领域层。」 业务逻辑的承载点,是业务价值的集中体现,通常构建于面向对象设计之上,基于封装、继承、多态等特性保障业务逻辑的复用性和扩展性;
「仓库层。」 主要用于数据访问,向上为应用服务提供数据操作服务,向下屏蔽各类存储引擎的差异;
「数据层。」 主要用于数据保存和检索,常见的数据存储引擎全部属于这一层,比如 mysql、redis、es 等;
其实,分层架构本身也是一种“拆分”,将不同的关注点封装在不同的层次。但除了横向分层,还可以基于 cqrs 对其进行纵向拆分,也就是将每个层的组件拆分为 command 和 query 两部分。

由于接入层冲突较小,本身拆分的意义不大,在此不做要求,但从严格意义上讲,仍旧建议进行拆分。

3. 应用服务层冲突与拆分
应用服务层拆分就是将一个应用服务拆分为 commandservice 和 queryservice 两组。
这样做可以避免很多不必要的麻烦,command 和 query 存在较大的区别,具体如下:
回想开篇时提到的场景,完成应用层拆分,就不在为使用错组件而烦恼:
commandservice 的 repository 不使用缓存,仅操作数据库
queryservice 的 repository 可以使用缓存,以提升访问性能
除此之外,针对统一的操作流程,还可以进一步抽象来消除重复的“模板代码”,比如:
1.引入“模板方法设计模式” 以达到核心逻辑的复用
抽象出 basecommandservice 和 basequeryservice 两个父类用于统一核心流程
子类实现 basecommandservice 和 basequeryservice 的抽象方法完成功能扩展
2.基于“约定优于配置” 使用 proxy 模型,只定义接口不写实现代码
按规范定义 commandservice 和 queryservice 接口,通过注解完成相关配置
自动生成 proxy 实现类,完成流程编排
4. 模型层冲突与拆分
模型层是系统的核心,它的设计直接影响整个系统的质量。作为承接业务逻辑的核心,比较流程的实现策略包括:
ddd 领域驱动设计,其核心是使用面向对象的高级特性(封装、继承、多态、组合等)来进行设计,非常适合复杂的业务场景。其体现就是存在很多高内聚低耦合的对象组(聚合根),业务逻辑由这些小对象相互协作共同完成;
事务脚本,使用过程式思维,将数据操作编织到流程中,比较适合并不复杂的业务场景。其体现就是存在很多“上帝 service”,service 中存在很多非常长的方法,业务逻辑由这些方法完成;
关于哪个才是最优解,网上已经争论多年,最终也没有结论。但我始终认为“没有业务场景就讨论方案,就是在耍流氓”。
从不同应用场景出发便可得到如下结论:
command 场景需要保障严谨的业务逻辑,通常复杂性偏高,所以ddd 是最优解
query 场景需要更灵活的数据组装能力作为支持,通常比较简单,所以 事务脚本 是最优解
我经常说:“最简单的“写”也是复杂,最复杂的“读”也是简单”,其背后逻辑是基于对 command 和 query 的场景判断。
将模型拆分为 command 和 query,具体如下:
完成模型拆分后,新模型具有以下特征:
agg 也就是 ddd 中聚合根,主要用于处理复杂的 command 逻辑,由具有大量业务操作的富对象构成;
view 是标准的 pojo,主要充当 query 结果对象,典型的“贫血对象”,仅作为数据的载体,根据展示需求对数据进行组装;
view 没有自己的 repository,只能依赖 commandrepository 获取数据,converter 组件负责将 agg 模型转换为 view 模型;
这块是拆分的重点,为了方便理解,简单举个例子:
比如在电商的订单模块:
生单流程,由 order 作为聚合根对内部 orderitem 和 payinfo 进行统一协调
订单列表页,只需展示 order 和 user 信息
订单详情,需要展示order、user、address、orderitem、payinfo、product等信息
如果让一个模型同时支持着三个场景,那模型自己就变的非常复杂,很难判断某个方法、某个字段究竟属于哪个场景。
此时,应该根据场景对模型进行拆分:
orderbo 以 ddd 方式进行建模,对外提供统一的业务操作,对内协调 orderitem 和 payinfo 等多个实体对象;
orderlistvo 以 pojo 方式进行建模,属性中包含 order 和 user 信息;
orderdetailvo 以 pojo 方式进行建模,属性中包括 order、user、address、orderitem、payinfo、product 等信息;
三个模型相互独立,互不影响。
当然,由于使用统一的 repository 还需提供对应 vo 的 converter:
orderlistvoconverter 将 orderbo 转换为 orderlistvo 对象
orderdetailvoconverter 将 orderbo 转化为 orderdetailvo 对象
5. 仓库层冲突与拆分
仓库层拆分也是非常有必要的,在这一层主要有几项冲突:
仓库拆分后整体架构如下:
仓库拆分具有以下特点:
view 不在需要 converter 组件完成数据转换
view 的数据来自于自己的 repository,可以根据展示需求进行灵活定制
command 和 query 仍旧使用同一套数据库、同一套数据表
6. 数据层冲突与拆分
数据层拆分是最重要的拆分,提到分离第一反应也是“数据库主从分离”。
数据层拆分的本质是:各种数据存储引擎的最佳应用场景相差巨大,读 和 写 优化往往存在矛盾。
仍旧以最常见的数据库为例:
提升查询性能,建议为各种查询维度建立索引
提升写入性能,需要让表上的索引越来越少
为了加速更新性能,建议使用三范式设计表结构,减少冗余信息
为了加速查询性能,建议使用反范式设计,尽量冗余数据,避免数据表间的 join 操作
鱼和熊掌不可兼得,在数据库层展示的淋漓尽致!
数据层拆分后架构如下:
该模型具有以下特点:
数据存储进行了彻底拆分;command 和 query 都可以灵活的选择最合适的存储引擎;
command 与 query 需要引入一套同步机制以完成两者的数据同步,常见的同步机制有:
工作在应用层基于领域事件的数据同步,如图所示
工作在数据层基于log的数据同步,如 mysql 的主从同步、canal2xx 等
数据层拆分是大型系统最终的归宿,仍旧以订单系统为例:
订单作为一致性要求极高的系统,command 侧首选仍旧为具有 acid 的关系型数据库,哪怕是分库分表底层存储仍旧不变;
为了满足高性能查询需求,需要在 query 侧引入 redis 作为分布式缓存对访问进行加速;
为了满足后台复杂且多维度的业务查询,需要在 query 侧引入 es 为全文检索进行加速;
为了满足各种实时报表需求,需要在 query 侧引入 tidb 以满足海量数据的实时检索;

这就是我们面临的现状:“数据密集型系统”越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来,通过 api 的方式,对外提供服务,屏蔽内部的复杂性。

7. 小结
“拆分”是“分离关注点”的重要手段之一。拆分的目的是将问题进行归类,然后采取有针对性的手段更好的解决问题。
cqrs 作为一种架构,将业务系统不同部分进行归类,接下来需要为 command 和 query 寻找最优解决方案:
「1、command,以 ddd 作为理论基础将战术模型中最佳实战进行落地,包括」
聚合设计
仓库设计
lazyload + context 模式
业务验证
领域事件

「2、query,以数据检索和组装作为核心能力,设计留给开发人员,实现留给框架,包括」
queryobject 查询对象模式
内存 join 模式
宽表&冗余表模式


Kubernetes容器垃圾回收的策略
硅钢片激光焊接的优势和难点
运用菱形十字搜索算法提高快速运动估计算法的性能
华为已经逐渐成为全球芯片企业的重要客户
触摸广告机如何应用在各行各业,它有哪些优势
cqrs是什么意思 DDD与CQRS的组合使用优势
如何选择合适的PCB打码设备?
LED显示看板发展趋势
广色域电视,广色域电视工作原理是什么?
基于Modbus协议与MCX314的自动钻孔系统设计
大功率LED照明散热的探讨
iPhone 8 Plus透明屏有哪些挑战和限制?
智慧汇聚 科技让物流更有“质”感
夏天电脑CPU太烫?“核心直触散热支架”了解一下
如何在Windows下使用 Supervisor 重新拉起崩溃的Python程序
工业以太网应用实例
Redmi K30 5G版将于1月7日开售搭载骁龙765G平台支持双模5G网络
物联网和人工智能结合打造最大化效益
光莆系列消杀产品打造安全无忧的赛场环境
什么智能家居的产品使用率最高