08.CQRS

CQRS

1.CQS

介绍

CQS(命令查询分离)一个方法要么读取要么写入,但绝不能同时进行两者,换句话说,提出一个问题不应改变答案

读与写

任何一个软件系统本质上就是读操作和写操作的集合,下图展示了两种操作的差异:

在日常开发中,可以把一个方法设计为纯查询模式或者纯命令模式,抑或两者的混合体。但在设计接口时,我们应该尽量使接口单一化,保证方法的行为严格遵循命令或者查询的操作语义。这样查询方法不会改变对象的状态,没有副作用,而会改变对象状态的方法不可能有返回值。查询功能和命令功能的分离,有助于提高系统性能,也有利于系统的安全

违反CQS的例子

违反CQS的常见例子就是从队列中出队,出队列方法会返回队列顶部的元素,像查询一样,但是会改变队列的状态,第二次调用出队的方法会给我们一个不同的结果,所以提出问题的行为也改变了答案

修复的方式

如果要修复上面的案例,那么出队方法就不能移除队列顶部的元素只是获取,然后再单独调用另一个方法,该方法移除该元素,但不返回任何内容

2.什么是CQRS

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将数据的读取操作与更新操作分离的架构模式

query指的是读操作,而command是对会引起数据变化的操作的总称,新增,删除,修改,导入,导出,重定向等这些操作都是命令

它的核心理概念是可以使用不同的模型来更新信息和读取信息。对于某些情况,这种分离是有价值的,但要注意对于大多数系统来说,CQRS会增加风险和复杂性

CQRS将读取和写入视为完全独立的子系统

3.为什么DDD需要CQRS

需求

在领域驱动设计中,除了单个聚合根的加载,在实际开发中常常会遇到如:分页,列表查询,条件查询,跨表查询等需求,这些需求可能需要同时查询多个聚合根的数据

问题

有的实践者会直接在仓储中实现这些数据查询的处理逻辑,但这样存在一些问题,还有一些问题是不引入CQRS导致的:

  1. 仓储职责不再单一:仓储的职责是为了维护单个聚合根的生命周期的,但这种实现会导致仓储承担了过多的查询职责,从而造成职责不在单一。这样做也意味着每次新增一个查询功能时,都需要在仓储中接口新增方法。为了保持仓储的职责单一,需要想办法在仓储之外支持这类查询

  2. 聚合的整存整取对于查询来说是个负担:条件查询等查询操作并不需要完整加载聚合根的状态。聚合根是事务一致性的边界,为了保证业务的准确性,仓储在加载聚合根时必须将聚合根的完整状态从数据库中加载出来。然而,在执行查询操作时,例如,分页列表查询,条件查询等操作,往往只需要读取聚合根的状态,而不会对聚合根的状态进行修改。有一些查询仅仅需要获得聚合根一部分字段,如果完整完整加载聚合根的所有状态,则可能会因为读取的数据过多而影响查询性能,并使得查询操作更加麻烦。因此,在只进行查询而不进行修改的场景下,其实没有必要完整加载聚合根

4.CQRS对于DDD的帮助

5.CQRS核心概念

介绍

一般来说,在DDD引入CQRS架构后,应用层根据职责被分为两部分:命令应用服务(Command Application Service)和查询应用服务(Query Application Service)

  • 命令应用服务:负责处理写操作,写操作通常会引起聚合根状态改变,例如创建,更新和删除书操作
  • 查询应用服务:负责处理读操作,包括查询和展示数据

命令侧(Command Side)

伪代码这里就省略了。前面的文章里面都有展示过聚合根是如何完成写操作的

查询侧(Query Side)

在查询应用服务中,使用数据模型执行查询操作(就是传统MVC的实体类+DAO),直接从数据库读取数据将其呈现给用户,Repository所在的基础设施层内部一般拥有数据模型和对应的ORM接口(比如Mybatis的Mapper),查询操作可以绕过领域模型,直接使用数据模型和ORM接口来完成操作,伪代码如下:

java 复制代码
/**
 * 查询应用服务伪代码
 */
public class QueryApplicationService {
    @Resource
    private DataMapper dataMapper;
    
    /**
     * 完成查询
     */
     public List<View> queryList (Query query) {
         // 获得查询条件
         Object condition1 = query.getCondition1();
         Object condition2 = query.getCondition2();
		// 直接调用Mapper完成查询
		List<Data> dataList = dataMapper.queryList(condition1,condition2);
		// 将数据对象转换成view对象
		List<View> viewList = this.toViewList(dataList);
		return viewList;
     }
}

事件总线

命令总线

在CQRS架构中,如果命令执行时间较长或请求并发量较高,但又无须实时获取命令的执行结果,就可以引入命令总线(Command Bus),将同步请求转为异步执行,从而提高响应能力。带有命令总线的CQRS模式和DDD融合过程如图所示:

总结

6.CQRS中的对象命名规范

名称 用于CQRS场景 角色(入参/出参) 说明
Command Command 入参 用于命令方法的入参
Query Query 入参 用于查询方法的入参
View View 出参 用于查询方法的出参

7.实现CQRS

介绍

CQRS的实现分为三个层面:方法级别的CQRS,相同数据源的CQRS,异构数据源的CQRS

方法级别的CQRS

其实就是说的CQS

方法级的CQRS不涉及数据源和模型,主要是对接口方法层面的约束:一个方法要么执行命令修改对象状态,要么进行查询返回数据,职责要单一,不应该在命令方法中返回查询结果,也不应该在查询操作中修改对象的状态

方法级别的CQRS实际上和DDD无关,不管有没有在实践DDD,都推荐将方法实现为CQRS

相同数据源的CQRS

介绍

只要命令和查询这两个操作使用的不是同一套模型,就已经实现了CQRS

在落地CQRS的过程中,可以先共用一套数据源同时处理读写请求,只不过命令操作由领域模型来完成,查询操作由数据模型来完成。同数据源的CQRS实现起来非常简单,其实已经能满足大部分的业务需要了

在实现同数据源的CQRS时,可以在DDD应用架构中将命令和查询分别使用不同的应用服务来实现,以此来区分

伪代码

省略,非常简单,按照图和过往文章可以很轻松实现出来

注意
  1. 在查询应用服务中,可以引入很多优化措施(如缓存)提升数据库查询性能
  2. CQRS在实现时,既可以将命令和查询是现在同一个应用中,也可以将命令和查询进行物理上的区分,分别实现在不同的应用中。Command那边用DDD来实现,Query那边用MVC来实现

异构数据源的CQRS

介绍

使用相同的数据源的CQRS基本上可以满足大部分业务场景的需求,然而有时为了解决特定问题,可能会引入其他数据中间件,将业务数据的副本存储到数据中间件中,导致数据异构存储。这时候就需要使用异构CQRS,即查询和命令两种操作由不同的数据源承接

注意
  1. 异构数据源不一定就是两种不同的数据中间件,还有可能两个都是MySQL但是表结构不一样
  2. 业界常见的一种实践是将用于查询操作的数据源设计为宽表,通过冗余字段的方式减少查询的性能消耗

8.CQRS的适用场景

  • 处理复杂的领域模型:一些复杂的领域模型可能难以使用传统的CRUD模式来管理,这时候使用CQRS可以让开发人员更容易地处理领域逻辑,并且将查询和写操作分开,使得系统更容易理解和维护
  • 高性能应用:CQRS可以让你将读写操作的负载分开,从而独立地扩展它们。如果你的应用程序看到读写操作之间有很大的差异,那么这是非常有用的。即使没有这种情况,你仍然可以对两个方面应用不同的优化策略。例如,使用不同的数据库访问技术来读取和更新
  • 总的来说,如果你的领域逻辑非常复杂,或者你的应用程序需要处理大量的读写负载,并且你希望能够独立地扩展每个方面,那么CQRS可能是一个非常好的选择

9.CQRS优缺点

优点

  • 分离业务关注点:命令操作关心聚合内一致性,而查询操作不需要关注一致性,两者的分离使得用代码实现时可以分别独立处理不同的业务需求,更方便地进行变更和迭代
  • 针对性地进行架构的优化:可以单独针对读或写进行优化,以便更好地处理高并发情况

缺点

  • 复杂性:实现CQRS模式需要更多的代码和额外的架构。这可能会导致项目变得更加复杂
  • 学习曲线:使用CQRS需要开发人员具备一定的领域驱动设计(DDD)知识,以及对命令查询职责分离(CQS)和事件驱动架构(EDA)等模式的了解。这可能需要更长的学习曲线,也可能需要额外的培训成本
  • 一致性:在异构数据源的实现方案中,数据的同步可能存在一定的延迟,进而导致数据的一致性问题,需采取合适的策略来解决数据一致性问题
  • 顺序问题:CQRS通常使用异步通信机制,这可能导致事件处理和数据更新的顺序问题,需要谨慎处理事件顺序和错误
  • 不适合所有系统:CQRS模式不适用于所有类型的系统。如果您的应用程序具有简单的CRUD(创建、读取、更新、删除)操作,那么使用CQRS可能会增加复杂性,但并不能提供任何实际的好处
相关推荐
南风lof6 分钟前
ReentrantLock与AbstractQueuedSynchronizer源码解析,一文读懂底层原理
后端
写bug写bug1 小时前
彻底搞懂 RSocket 协议
java·后端
就是我1 小时前
轻松管理Linux定时任务:Cron实用教程
linux·后端
橘子青衫1 小时前
深入理解Callable与Future:实现Java多线程中的异步任务处理
java·后端
bobz9651 小时前
libvirt 相关 sock 整理
后端
Asthenia04122 小时前
ElasticSearch8.x+SpringBoot3.X联调踩坑指南
后端
gou123412343 小时前
【Golang进阶】第八章:并发编程基础——从Goroutine调度到Channel通信实战
开发语言·后端·golang
程序小武3 小时前
python编辑器如何选择?
后端·python
陈随易3 小时前
薪资跳动,VSCode实时显示今日打工收入
前端·后端·程序员
失乐园3 小时前
电商/物流/IoT三大场景:用MongoDB设计高扩展数据架构的最佳实践
java·后端·架构