【系统架构】对CQRS的基础理解

CQRS由Greg Young提出,目前在DDD领域中被广泛使用。在我看来,它甚至可以被称为是一种架构风格,可以取得与MapReduce,REST同等的地位,对软件系统的整体架构产生重要影响。

CQRS即Command Query Responsibility Seperation(命令查询职责分离),其设计思想来源于Mayer提出的CQS(Command Query Seperation)。这种命令与查询的分离方式,可以更好地控制请求者的操作。查询操作不会造成数据的修改,因而它属于一种幂等操作,可以反复地发起,而不用担心会对系统造成影响。基于这种特性,我们还可以为其提供缓存,从而改进查询的性能。命令操作则与之相反,它会直接影响系统信息的改变。

查询操作与命令操作对事务的要求也不一样。由于查询操作不会改变系统状态,因而,不会产生最终的数据不一致。从请求响应的角度来看,查询操作常常需要同步请求,实时返回结果;命令操作则不然,因为我们并不期待命令操作必须返回结果,这就可以采用fire-and-forget方式,而这种方式正是运用异步操作的前提。此外,对于大多数软件系统而言,查询操作发起的频率通常要远远高于命令操作。如上种种,都是将命令与查询进行分离的根本原因。

这就很好地阐释了我们为何需要运用CQRS模式,同时也说明了CQRS的适用场景。

只要充分理解了运用CQRS模式的意图,理解CQRS模式就变得容易了许多。下图是CQRS框架AxonFramework官方文档给出的CQRS架构图。

在这个架构图中,最核心的概念是Command、Event。以我的理解,CQRS模式的风格源头就是基于事件的异步状态机模型。抛开命令查询分离这一核心原则,这才是CQRS的基础内容。

CQRS对设计者的影响,是将领域逻辑,尤其是业务流程,皆看做是一种领域对象状态迁移的过程。这一点与REST将HTTP应用协议看做是应用状态迁移的引擎,有着异曲同工之妙。这种观点(或设计视图)引出了Command与Event的概念。Command是系统中会引起状态变化的活动,通常是一种命令语气,例如注册会议RegisterToConference。至于Event,则描述了某种事件的发生,通常是命令的结果(但并不一定是直接结果,但源头一定是因为发送了命令),例如OrderConfirmed。

我发现,这种事件更接近于一种事实,即某次数据改变的结果,是一种确定无疑已经发生的事实。这一思想直接引入了Event Source,并带来Audit(审计)的好处。而它更是与Datomic数据库的设计哲学一脉相承。Datomic的设计哲学就是:“将数据(Data)看做是事实(Fact)。每个事实都是过去的痕迹,虽然这种过去可以遗忘,但却无法改变。”Event Source可以将这些事件的发生过程记录下来,使得我们可以追溯业务流程。

Command和Event都有对应的Handler来处理。它们具有一个共同的特征,即支持异步处理方式。这也是为何在架构中需要引入Command Bus和Event Bus的原因。

在UI端执行命令请求,事实上就是将命令(注意,这是一个命令对象,你完全可以将其理解为Command模式的运用。注意,命令的命名一定要恰如其分地体现业务的意图)发送到Command Bus中。Command Bus更像是一个调停者(Mediator),在接收到Command时,会将其路由到准确的CommandHandler,由CommandHandler来处理该命令。在Axon Framework中,Command Bus提供了dispatch()方法对命令进行分发。也就是说,在它的实现中,并没有对Command提供异步处理,而仅仅是完成路由的功能。当然,我们未必要拘泥与这种设计思路。例如,我们可以将Command Bus看做是消息通道,而将Command Handler看做是该消息通道的侦听者。因此,可以引入队列来实现Command Bus。

Event的处理与之相似。Axon Framework同时支持同步和异步方式。从框架角度讲,提供更多的选择是一件好事。但基于CQRS模式的核心思想来看,如果对Command(包括Event)的处理未采用异步模型,它就没有发挥出足够的优势,此时采用CQRS,反而会增加设计难度,有些得不偿失。

在Command端,基本的处理流程是由UI发起命令请求,发送到CommandBus,并由它分发给对应的Command Handler来处理命令。Command Handler会与领域对象,特别是与Aggregation Root对象通信。在处理了相关的业务逻辑后,会触发Event。一方面,它会将Event放到Event Store中;另一方面,同时会将Event发送到Event Bus,再由Event Handler处理事件。根据Axon Framework的官方文档,Event Handler会负责更新数据源,从而保证查询端能够得到最新的数据。

然而,这一过程未必能这样简单。因为整个过程可能体现的是一个状态机。Command会导致状态的迁移,并在执行Aggregate的逻辑时,触发对应的Event。Event Handler在处理事件时,并不一定是这个业务过程的终点,它可能会发送引起下一个状态迁移的命令,从而形成一个不断迁移的过程,直至业务完全结束。这就需要我们在引入CQRS时,需要改变之前的设计思路,尽量从状态迁移的角度去理解业务逻辑。UML中的状态图是一个很好的分析工具。

它也带来一个挑战,就是事务。因为整个过程都涉及到数据状态的变化,当某个状态迁移出现问题时,要保证数据的最终结果是一致的。Axon Framework的解决方案是引入Unit of Work模式。此外,在真正实现时,究竟是由Event Handler去更新数据源,还是交由Aggregate去完成,还有待考量。我倾向于由Aggregate委派给Repository来完成。从职责分配的角度来看,这种方式更为合理。因为与数据源打交道的逻辑绝对不能太过于分散,以免数据源的改变影响到整个领域层。在DDD中,持久逻辑都是被封装到Repository(在其内部,又会委派给基础设施层中提供数据访问的对象)。换言之,这种实践是符合DDD的设计思想的。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2014-08-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Ksher

Kubernetes服务网格(第10部分):服务网格API

翻译人:Ksher,该成员来自云+社区翻译社

19210
来自专栏desperate633

TCP/IP之IP编址IP子网( Subnets)有类IP地址IP子网划分与子网掩码

IP地址: 32比特(IPv4)编号标识主机、路由器的接口 ** IP地址与每个接口关联 **

854
来自专栏无题

高并发商品详情页构建

* 主要思路: 1、数据变更还是通过MQ通知; 2、数据异构Worker得到通知,然后按照一些维度进行数据存储,存储到数据异构JIMDB集群(JIMDB:Re...

3526
来自专栏大魏分享(微信公众号:david-share)

浅谈业内各种主流双活存储技术,以及开源的HA/DR方案

声明 本文为笔者根据自身的项目经验,以及参考大量文档书写而成。文中提到很多厂商的解决方案和概念,第一不方案之间的优劣评判,第二不进行厂商间的相互攻击,第三文中仅...

3376
来自专栏Golang语言社区

Go 语言构建高并发分布式系统实践

你知道互联网最抢手的技术人才有哪些吗?最新互联网职场生态报告显示,最抢手的十大互联网技术人才排名中Go语言开发人员位居第三,从中不难见得,Go语言的渗透率越来越...

3594
来自专栏软件测试经验与教训

接口分类

2678
来自专栏张善友的专栏

通用日志

一、背景 为软件开发提供一个现成的、定义良好的、可扩展的日志设施。所谓"现成的"意思为软件开发可以即刻使用,包括API文档、使用实例和库;"定义良好的"表示项目...

18810
来自专栏数据和云

问诊白求恩之性能分析:把握趋势比你更了解你的库

如果问你,你的数据库性能如何,你会怎么回答呢? DBA 甲: db file sequential read等待事件经常出现,不知道什么原因。 DBA 乙:平常...

3095
来自专栏Java架构沉思录

大白话讲解CAP定理

分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。

773
来自专栏云计算

Kubernetes服务网格(第10部分):服务网格API

作为上个月发布的Linkerd 1.0的一部分,我们发现一些人已经开始注意Linkerd的服务网格API。随着1.0版本的发布,我们认为需要花些时间来解释这个A...

2126

扫描关注云+社区