Command Query Responsibility Segregation Pattern

在面向对象设计中,常常忽略了一个概念,对象是用于描述一个单一职责的,它要求我们:一个对象包含自身属性、行为、内在调用等行为。对象间通过相互调用完成一项工作。因此,我们在做面向对象设计时,应该分清一个对象应该包含多少东西,对象的属性是否 belong to它自身。如果一个对象的行为模糊不清,是否该对象设计合理?

什么是CQRS?

CQRS即 Command Query Responsibility Segregation——命令查询职责分离。该术语描述了一个对象的方法应该 要么是 commands 要么是 queries

  • 一个 query 返回数据但不会更改对象的状态。
  • 一个 command 改变对象的状态,但不会返回任何数据。

从该术语的原则性上带来的好处是:系统状态发生改变时,可以清晰理解状态的变更做了什么。

CQRS在该原则上更进一层,并定义了一个简单的模式。

前面说到,一个对象的方法要么是 commands 要么是 queries, CQRS则是从一个对象分离两个对象的实现。这种分离的依据是方法是 command 还是 querya command is any method that mutates state and a query is any method that returns a value)。

在构建企业级系统中,这种简朴的模式带来什么重要的令人关注的方面?主要是迎合大规模、大范围架构的挑战,诸如:

  • 实现可伸缩性
  • 管理复杂性
  • 已有系统业务部分内容频繁变更

“CQRS is a simple pattern that strictly segregates the responsibility of handling command input into an autonomous system from the responsibility of handling side-effect-free query/read access on the same system. Consequently, the decoupling allows for any number of homogeneous or heterogeneous query/read modules to be paired with a command processor. This principle presents a very suitable foundation for event sourcing, eventual-consistency state replication/fan-out and, thus, high-scale read access. In simple terms, you don’t service queries via the same module of a service that you process commands through. In REST terminology, GET requests wire up to a different thing from what PUT, POST, and DELETE requests wire up to.”
—Clemens Vasters (CQRS Advisors Mail List)

“大意:CQRS的模式将 command处理的职责分离出来,实现自治。并且允许任意数量的同构或异构的 query/read 搭配到 command processor。该原则非常适用于event sourcing的实现——最终一致性状态 应答/扇出、大规模read访问。简言之,服务通过command处理,不在同一个模块提供 query。 ”

Read and write sides

下图是 CQRS 在企业级系统的典型应用。

cqrs

CQRS 模式中实现了读写分离。read side 仅包含 query methods;write side 仅包含 command methods。这样分离的动机主要考虑到:

  • 在多数业务系统,读和写的数量是不对等的(异构的)。每个 write 可能同时处理上千个 reads。分离这两部分使得我们能够单独进行优化。例如,对 reads部分进行水平扩展…
  • 典型地,commands涉及到复杂业务逻辑,要确保系统能写入正确的、一致的数据到数据存储中。read操作比起write操作要简单许多。一个简单的同时封装 readwrite 操作的模型(例如 JPA)可能两方面都处理不当。从最终结果分离 readwrite,得到更容易维护、更灵活的模型。
  • 分离也可以从数据存储上着手。write部分使用满足第三范式(3NF)的数据架构;read部分使用非规范型数据库用于优化得到更快速的query操作。

注意:
上图描述了CQRS在readwrite使用了不同的数据存储,不是要求你使用不同的数据库。它仅仅描述CQRS这个模型,鼓励你分离读写部分。
上图建议writeread可能存在一一对应(one-to-one)关系,实际上没有必要要求建立这种关系。仅当你使用接口时,这种一一对应关系才明显。

在实践和适应这种架构的同时,可能会潜在一系列问题:

  • 尽管单独readwrite两个模型比起一个复合模型要简单,当业务量需求扩增时,整体的架构要比传统的方式要复杂。如何转移该复杂性?
  • readwrite两方面,如何传播数据的变更?
  • write部分若发生延迟,如何传递到read部分?
  • CQRS的模型是什么?它能做什么?

CQRS and DDD

领域驱动(Domain-Driven Design)设计理念要求有:

  • Models should be bound to the implementation.
  • You should cultivate a language based on the model.
  • Models should be knowledge rich.
  • You should brainstorm and experiment to develop the model.

当讨论在系统中实现CQRS,意味着你应该实现一个带有bounded context的CQRS模式。

在DDD,bounded context定义了一个模型的语义、通用语言(ubiquitous language)的作用域(scope)。实现CQRS模式理所当然带来了 scalability、simplicity、maintainability。因此,在讨论 bounded context时候,CQRS模式用于实现业务组件,DDD模式用于实现上下文边界(bounded context)。

a business component can exist in only one bounded context.

以前在DDD概念中,bounded context还没有一个比较明确的定位,术语bounded contextbusiness component表述的是同一个东西。引入CQRS模式后,术语bounded context优于business component使用。

总之,你不应该将CQRS模式用于系统的顶层设计。CQRS应该明确定义在系统中独立的、具有明显业务逻辑的功能。

Commands, Events, Messages

DDD 属于一种分析和设计方法,它鼓励在使用模型(models)通用语言(ubiquitous language)的基础上,使用领域的概念培养开发团队的共识, 构建业务开发团队的桥接。必然地,DDD的方式面向分析行为,而不是业务领域中的数据,它导向于行为的建模和实现。较直接实现领域模型(domain model)的方式,就是使用commandsevents

  • commands是祈使句:它由系统发起处理一个任务或动作。命令通常处理一次。
  • events 是通告(notifications):告知某事已经发生。事件可以发生多次,被多处消费。

commandsevents都属于message,都用于对象间数据交换。在DDD术语中,message表示业务行为,帮助系统捕获业务意图。

CQRS的一个可能的实现方式是分离readwrite存储;每个数据存储被优化。Event提供了一个基本的机制用于异步writeread操作。当write部分发起一个事件表示应用状态变更,read部分响应该事件,并更新数据。下图为命令和事件在CQRS模式中的实现。

Commands and events in the CQRS Pattern

为什么用CQRS?

回到DDD领域设计,CQRS适用于DDD的bounded context允许标识和专注于系统逻辑最为复杂部分的实现。CQRS仅仅是bounded context一个具体的实践,但不是唯一的模式。

对于业务逻辑带来以下几点好处:

Scalability

在多数企业系统中,读操作远超过写操作,因此,读写操作的扩展要求是不一样的。在bounded context中分离readwrite,成为单独的模型,可以单独扩展各自的功能。

另外,伸缩性并不是实现CQRS模式的唯一理由:在一个非协作领域,你可以水平地添加更多的数据库来支持更多用户、更多的请求。

Reduced complexity

在领域的复杂区域,设计和实现对象,都会加剧复杂性。大多数情况下,复杂的业务逻辑仅仅在处理更新或事物处理上发生;相反,读的逻辑通常很简单。当读操作和其它业务逻辑混淆在相同的模型时,就变得难于处理问题,诸如多用户、共享数据、性能、事物、一致性、脏数据。因此要分离read logicbusiness logic到不同的模型中。然而,这种分离的代价是需要花费较大的努力,并且要求开发者自身对已有的模型有充足的理解。

分离职责是CQRS的核心动机,因为query会被用于非常多的场景、会被各处引用,每个修改都需要特别小心。如何设计一个复杂度更低的系统,正是bounded context的潜力所在,你可能需要TDD、refactor、或更多优秀的编码习惯来贯穿。

Flexibility

CQRS的灵活性,来源于read-sidewrite-side模型的扩展。read-side的扩展非常容易,因为读操作复杂度最低,并且不会受到业务逻辑的影响。对于write-side,它有单独的模型处理业务逻辑,模型相互独立,不依赖其它核心逻辑(从设计上来说,如果有依赖,要么设计有问题,要么面临重构)。

从长远来看,业务逻辑的灵活性直接与商业价值挂钩。它决定了你是否能够持续交付新功能、能否进行敏捷开发、能否满足同行的恶劣竞争。

灵活性和敏捷性与DDD的持续集成概念相关:

“Continuous integration means that all work within the context is being merged and made consistent frequently enough that when splinters happen they are caught and corrected quickly.”
—Eric Evans, “Domain-Driven Design,” p342.

Focus on the business
Facilitates building task-based UIs

When should I use CQRS?

什么时候用CQRS?微服务。CQRS模式明显是用于bounded context中的。bounded context的另一个术语是business components。说白了就是我们要实现的服务,具体是什么服务?如何划分?服务之间的相互关系是怎样?这都属于bounded context中描述的内容。CQRS可能不是最好的,但它提供了 increased adaptability、flexibility、reduced maintenance costs各方面的优势。

Collaborative domains

DDD概念需要解决的问题是如何描述有界上下文,开发者要与领域专家进行交流,就需要明确这个bounded context是什么。

CQRS参与了涉及复杂决定的协作过程——结果(产出)什么。这种协作往往是系统中最复杂、不固定、最需要关注的bounded contexts

Stale data

在协作环境中,多个用户可能同时操作同一份数据,你将面临脏数据问题;如一个用户正在浏览数据,另一个用户在进行更改,那么第一个的用户看到的是脏数据。

前面两个举例是最常见的场景;大多数协作企业系统比这更多。CQRS从架构层面上解决脏数据的问题。我们切换到write-side,用户从read-side读取数据。无论用什么机制,将write-side的数据 pushread-sideread-side就以相同的机制操作数据。常见的机制就是ES(Event sourcing)

Event Sourcing

When should I avoid CQRS?

非协作的、简单的、静态的用于处理分析、建模之类的复杂实现bounded contexts。应该避免使用CQRS。