尺有所短,寸有所长;不忘初心,方得始终。
在数据库设计中经常会提到读写分离,为了提升系统的性能,我们将读操作与写操作分开作用在不同的库中。在DDD中引入了CQRS的概念,设计软件之初将软件模型分为读模型和写模型。
一、什么是CQRS
命令查询职责分离:简称CQRS(Command Query Resposibility Segregation)。它是将软件设计中的命令-查询分离并应用在架构模式中的设计原则。
在《Object-Oriented Software Construction》书中有一个概念:
Every method should either be a command that performs an action, or a query that returns data to the caller, but never both.
译文:一个方法要么作为一个【命令】执行一个操作,要么作为一次【查询】向调用方返回数据,但两者不能共存。
CQRS从模型层面,将即【命令】和【查询】分别使用不同的对象模型来表示。
【命令】:Command ,修改了对象的状态
不应该返回数据,在Java中,方法应该声明为void。
【查询】:Query ,不应该通过直接或者间接的方式修改对象状态
返回数据,在Java中,方法应该声明返回的数据类型。
在DDD架构中,通常会将查询和命令操作分开。具体落地时,可以将Command和Query分成两个工程,但是大多数情况下放在一个项目可以提高业务内聚性。这个可以根据实际情况决定。以下是一张CQRS官网的模型图
图解:
这张图读写体现的是逻辑分离,物理层面使用的是同一个数据库,而实际上可以将数据库改成读库和写库的物理分离,只需要同步两个库即可。常用的解决方案是当写库发生更改时,通过Event事件机制通知读库进行同步。 在实际的CQRS落地时,我们即使物理层面使用的是同一个数据库,但是可能还是会用到其他的物理存储,比如Elasticsearch,Redis等。当数据库发生更改时: 发送Event事件通知ES/Redis进行数据更新同步。 通过监听Mysql的binlog更新ES/Redis。
二、CQRS实现方式
2.1 读模型的数据源
上面提到读写模型可以将Command和Query分成两个工程使用不同的物理库,也可以放在一起使用同一个物理
库。无论是那种方式数据的来源应该都来自于业务实体对象(比如聚合根)。
读模型的数据源形式分为:
单进程单实体:数据来源于同一个进程空间的单个实体 单进程跨实体:数据来源于同一个进程空间中的多个实体 跨进程跨实体:数据来源于不同进程空间中的多个实体
进程空间:指某个单体应用或者单个微服务。
2.2 读写分离的形式
读写分离的形式基于数据来源的不同,CQRS可以在代码中实现读写分离(共享模型/分离模型),也可以在物理存储中实现读写分离(共享存储/分离存储)。两种方式是结合起来使用的:
共享存储/共享模型
共享存储(读写同一个数据库),共享模型(读写同一个领域服务,领域对象),数查询据通过模型转换后返回给调用方。
共享存储/分离模型
共享数据存储,代码中分别建立写模型和读模型,读模型是基于查询方式进行的建模。
分离存储/分离模型
数据存储和代码模型都是分离的,通常用于需要聚合查询多个子系统(比如微服务系统)。
2.3 CQRS模式
在DDD实践中,CQRS模式是由【读模型的数据源】与【读写模型的分离形式】组合使用。
主要分为:
单进程单实体 + 共享存储/共享模型 单进程单实体 + 共享存储/分离模型 单进程跨实体 + 共享存储/分离模型 单进程跨实体 + 分离存储/分离模型 跨进程跨实体 + 分离存储/分离模型
通过以上5中方式基本上可以覆盖常用的一些业务场景。
2.3.1 单进程单实体 + 共享存储/共享模型
对于简单的单体或者微服务应用,这种方式是最直接的,在单个领域实体模型同时用于读写操作,在向调用方返回查询数据时,只需要转换相应的领域模型即可。
2.3.2 单进程单实体 + 共享存储/分离模型
当单个实体的查询很复杂时,可以对读模型和写模型分别建模,以此维护读写过程彼此的清晰性。
2.3.3 单进程跨实体 + 共享存储/分离模型
在同一个进程空间中的跨实体查询时时候可以使用分离模型的形式。做join联合查询即可。
2.3.4 单进程跨实体 + 分离存储/分离模型
当系统数据量比较大,并发比较大,对性能要求比较高得时候,我们可以采用分离存分离存储/分离模型的方式,
采用专门的数据库来简化查询提升效率。
2.3.5 跨进程跨实体 + 分离存储/分离模型
在微服务中,每个服务都内聚性地管理自身的聚合根对象,意味着分离模型。而微服务的数据存储如果是独占式的,则数据存储一定是分离的。
在这种方式中,查询的数据通常通过事件机制从不同的其他业务服务中同步得到,通过API向外暴露。
三、DDD、CQRS架构落地
根据上面的分析,在DDD+CQRS的架构落地时,Web、RPC、DB、MQ等基础设施层会依赖内部的抽象,属于一个对称性架构
所有的抽象都定义在圆圈内部,实现都在基础设施。而针对读写的应该都在应用层划分:
当一个命令Command请求过来时,会通过应用层的CommandService去协调领域层工作,
当一个查询Query请求过来时,则直接通过基础实施的实现与数据库或者外部服务交互。
在实际开发中,我们会发现Query和Command的有一些数据和抽象服务是公用的,为了提高代码的复用性,可以单独抽离出来一个公用的数据对象和抽象模块Shared
- END -