大家好,欢迎来到IT知识分享网。
什么是CQRS
CQRS是DDD中的一个重要设计。DDD(领域模型)是现在比较火的一个系统分析的方法论。「领域」在 DDD 中占据了核心的地位,DDD 通过领域对象之间的交互实现业务逻辑与流程,并通过分层的方式将业务逻辑剥离出来,单独进行维护,从而控制业务本身的复杂度。
但是,问题来了,业务系统的查询是复杂多变的,很难用一个模型来涵盖所有复杂的场景。比如说教育系统中,一个题目的查询功能,可能数据来源于「题目」,「题目来源」,「知识点标注」,「试卷引用」,「学生作答」等多个领域模型中的几个字段。这样的场景如果还是通过领域对象来封装就显得很麻烦,其次与领域知识也没有太紧密的关系。此时,我们就需要用另一个手段来解决这个问题。它就是CQRS。
CQRS — Command Query Responsibility Segregation(命令查询职责分离),故名思义是将 command 与 query 分离的一种模式。query 很好理解,就是我们之前提到的「查询」,那么 command 命令又是什么呢?命令就是对会引起数据发生变化操作的总称(增、删、改),而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。规模复杂的系统还可以将命令与查询分成两个系统分开部署,分别进行系统优化、横向扩展等,甚至连数据源都可以完全分开。
架构图
下图是CQRS框架AxonFramework官方文档给出的CQRS架构图。
在这个架构图中,最核心的概念是Command、Event。当 Command 系统完成数据更新的操作后,会通过「领域事件」Event的方式通知 Query 系统。Query 系统在接受到事件之后更新自己的数据源。所有的查询操作都通过 Query 系统暴露的接口完成。
个人认为CQRS模式对设计者的影响,是将领域逻辑,尤其是业务流程,皆看做是一种领域对象状态迁移的过程。这一点与REST将HTTP应用协议看做是应用状态迁移的引擎,有着异曲同工之妙。这种观点(或设计视图)引出了Command与Event的概念。Command是系统中会引起状态变化的活动,通常是一种命令语气,例如注册会议RegisterToConference。至于Event,则描述了某种事件的发生,通常是命令的结果(但并不一定是直接结果,但源头一定是因为发送了命令),例如OrderConfirmed(订单确认)。这种事件更接近于一种事实,即某次数据改变的结果,是一种确定无疑已经发生的事实。这一思想直接引入了Event Source,并带来Audit(审计)的好处。而它更是与Datomic数据库的设计哲学一脉相承。Event Source可以将这些事件的发生过程记录下来,使得我们可以追溯业务流程。
Datomic的设计哲学就是:“将数据(Data)看做是事实(Fact)。每个事实都是过去的痕迹,虽然这种过去可以遗忘,但却无法改变。”
说得好像有点有点复杂,但从架构图上来看,CQRS 的实现似乎并不难,许多开发者觉得无非是「增删改」一套系统一个数据库,「查询」一个系统一个数据库而已,有点类似数据库中的「读写分离」,并没有什么特别的地方。但是真正要使用 CQRS 是有许多问题与细节要解决的。
几种CQRS的实现
一 共享数据库、共享数据模型
适用于数据模型的结构并不太复杂,查询和命令可以用同一个[领域模型]覆盖。严格意义上讲,这并不是一个CQRS架构,而是一个简单的基于数据库的增删改查。CQRS最重要的一个特点是读写分离,读应用和写应用分开部署。此架构只在上层做了区分,共用一种数据模型,在解耦合性,可扩展性都会相对较弱。适用于简单的业务,一种数据模型可覆盖所有业务。比如一些单表业务。
例如pv日志系统。应用端直接写入pv数据到ES(假设这里不做异步收集),kibana负责聚合查询数据,此场景下共用了一个ES的数据模型。当查询端或命令端有大量的内存计算时可以考虑。
二 共享数据库、分离数据模型
这是一个很常见的使用模型,也是开发和维护成本相对较低的模型。其架构如下:
这种方式适用的场景比较多,尤其是涉及到多表聚合的场景。例如题目管理系统中题目的录入和标注知识点,这是对两个不同的表的命令操作。而题目的展示往往会将两种数据进行聚合,一起展示。所以就需要根据展示的内容重新梳理一个模型(read domain),查询根据新梳理的模型来进行查询业务。读写模型都来自于同一数据源。此设计同样适用于复杂的写入事务同时只有简单的查询等场景。
三 分离数据库、分离数据模型
这个在复杂的高性能系统中比较常见。在阿里规约中曾经提到:“【强制】超过三个表禁止 join ”。多表查询往往会提高接口的响应时间。那怎么规避这个问题呢?我们往往会将数据进行冗余,使多表查询变成单表查询。当冗余字段的方式达不到我们的要求时,我们就需要重新定义一个冗余表,来处理复杂查询业务,以获得高性能高吞吐(空间换时间)。这种架构就类似于上面提到的AxonFramework官方文档给出的CQRS架构图了。
两个数据存储怎么同步呢?这里就用到事件驱动模型。
事件驱动模型
事件驱动模型,也即是我们通常说的观察者。基于发布-订阅模式的编程模型。定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都得到通知并自动更新。也就是上图AxonFramework中的Event。通知的处理方式可以是同步的也可以异步的,可以开子线程,还可以借助消息队列(消峰,事件有顺序性)。更有些大数据架构会把event也分离出来,作为一个单独的系统,比如阿里Canal,DataX,Flink等等,通过监听数据库的日志(binlog)来实现数据同步。
例如一个题目的搜索系统,涉及到了关键字分词查询,复杂的聚合,传统的数据库无法满足业务。所以我们通过监听写数据源Mysql的事件,同步到ES中进行复杂搜索。
需要考虑问题
我们在享受CQRS这些优点(高扩展,低耦合,高性能)的同时,这种方式也会带来很多的问题。
1 事务
这个是非常棘手的问题,因为成熟的CQRS模型往往读写完全分离,通过消息队列来进行异步同步。如何保持数据的一致性,这是此架构的难点。通常设计者面对这个问题,往往会采用几种方式。
- 分布式事务:可以使用TCC(事务补偿),两段提交等方式,开发难度比较大。
- 最终一致性:在业务上可以忽略数据在短时间内的不一致,保证数据的最终一致。
- 躺平:先不管数据的不一致,每天安排定时任务进行修正。比如说一些定时的报表。
2 查询模型的设计
虽然 CQRS 为我们分离了领域模型和服务于查询功能的数据模型,但这意味着我们需要设计另一套针对查询功能的数据模型。一般比较简单的做法是按照查询功能所需的数据进行设计,即针对每一个查询接口设计一个数据视图,当收到领域事件时更新有关联的数据视图。
但是这种简单做法带来的问题就是当查询接口越来越多时就越来越难以管理,仍然需要按照 DDD 中划分 BC 的思路将属于一个 BC 的查询集中管理作为整个查询系统的一个上下文,或是干脆独立出来做一个微服务。所以即使引入了 CQRS,我们依然需要使用领域驱动的思路设计查询接口。
3 消息中间件的管理
当业务系统达到一定的复杂度后,会发现项目充斥着大量的事件消息,如果对这些消息疏于管理,那么很可能出现消息混乱,互相干扰。同时一些操作频繁的事件,消息量会很大,可能会拖垮其他的消息同步,甚至拖垮消息中间件。所以消息中间件的引入,会增加开发维护成本。我们在设计上会使用快慢队列、滑动窗口等方式对消息进行一些缓冲处理,从而提高系统的稳定性。
小结
CQRS 在 DDD 中是一种常常被提及的模式,它的用途在于将领域模型与查询功能进行分离,让一些复杂的查询摆脱领域模型的限制,以更为简单的 DTO 形式展现查询结果。同时分离了不同的数据存储结构,让开发者按照查询的功能与要求更加自由地选择数据存储引擎。同时,CQRS 在带来架构自由与便利的同时也不可避免地引入了额外的复杂性与技能要求,例如对于分布式事务,消息中间件的管理,数据模型的设计等等,所以在引入 CQRS 之前需要对团队能力与现有架构做仔细的分析,对短板进行必要的提升。
所以,如果现有系统逻辑较为简单,只是一些 CRUD,那么并不建议使用 CQRS。但是如果你的业务系统已经非常庞大,业务流程庞杂,逻辑繁琐,那么不妨尝试使用 CQRS 将 Command 与 Query 进行拆分,将领域模型与数据模型的边界划分得更清晰些。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/165549.html