CQRS架构

命令查询的责任分离Command Query Responsibility Segregation (简称CQRS)模式是一种架构体系模式,能够使改变模型的状态的命令和模型状态的查询实现分离。这属于DDD应用领域的一个模式,为了使得项目逻辑更加清晰,便于对不同部分进行针对性的优化。

一. 背景问题

在以前的管理系统中,命令(Command,通常用来更新数据,操作DB和查询(Query)通常使用的是在数据访问层中Repository中的实体对象(这些对象是对DB中表的映射),这些实体有可能是SQLServer中的一行数据或者多个表。
通常对DB执行的增,删,改,查(CRUD)都是针对的系统的实体对象。如通过数据访问层获取数据,然后通过数据传输对象DTO传给表现层。或者,用户需要更新数据,通过DTO对象将数据传给Model,然后通过数据访问层写回数据库,系统中的所有交互都是和数据查询和存储有关,可以认为是数据驱动(Data-Driven)的,如下图:
这里写图片描述
对于一些比较简单的系统,使用这种CRUD的设计方式能够满足要求。特别是通过一些代码生成工具及ORM等能够非常方便快速的实现功能。
但是传统的CRUD方法有一些问题:
• 使用同一个对象实体来进行数据库读写可能会太粗糙,大多数情况下,比如编辑的时候可能只需要更新个别字段,但是却需要将整个对象都穿进去,有些字段其实是不需要更新的。在查询的时候在表现层可能只需要个别字段,但是需要查询和返回整个实体对象。
• 使用同一实体对象对同一数据进行读写操作的时候,可能会遇到资源竞争的情况,经常要处理的锁的问题,在写入数据的时候,需要加锁。读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并且会对系统吞吐量的增长会产生影响。
• 同步的,直接与数据库进行交互在大数据量同时访问的情况下可能会影响性能和响应性,并且可能会产生性能瓶颈。
• 由于同一实体对象都会在读写操作中用到,所以对于安全和权限的管理会变得比较复杂。
这里面很重要的一个问题是,系统中的读写频率比,是偏向读,还是偏向写,就如同一般的数据结构在查找和修改上时间复杂度不一样,在设计系统的结构时也需要考虑这样的问题。
解决方法就是我们经常用到的对数据库进行读写分离。 让主数据库处理事务性的增,删,改操作(Insert,Update,Delete)操作,让从数据库处理查询操作(Select操作),数据库复制被用来将事务性操作导致的变更同步到集群中的从数据库。这只是从DB角度处理了读写分离,但是从业务或者系统上面读和写仍然是存放在一起的。他们都是用的同一个实体对象。
要从业务上将读和写分离,就是接下来要介绍的命令查询职责分离模式。

二. 什么是CQRS

CQRS最早来自于Betrand Meyer(Eiffel语言之父,开-闭原则OCP提出者)在 Object-Oriented Software Construction 这本书中提到的一种 命令查询分离 (Command Query Separation,CQS) 的概念。其基本思想在于,任何一个对象的方法可以分为两大类:
命令(Command):不返回任何结果(void),但会改变对象的状态。
查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。
根据CQS的思想,任何一个方法都可以拆分为命令和查询两部分,比如:

private int i = 0;
private int Increase(int value)
{
    i += value;
    return i;
}

这个方法,我们执行了一个命令即对变量i进行相加,同时又执行了一个Query,即查询返回了i的值,如果按照CQS的思想,该方法可以拆成Command和Query两个方法,如下:

private void IncreaseCommand(int value)
{
    i += value;
}
private int QueryValue()
{
    return i;
}

操作和查询分离使得我们能够更好的把握对象的细节,能够更好的理解哪些操作会改变系统的状态。当然CQS也有一些缺点,比如代码需要处理多线程的情况。
CQRS是对CQS模式的进一步改进成的一种简单模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。“CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)”。
CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的。这样读和写逻辑就隔离开来了。
这里写图片描述
使用CQRS分离了读写职责之后,可以对数据进行读写分离操作来改进性能,可扩展性和安全。如下图:
这里写图片描述
主数据库处理CUD,从库处理R,从库的的结构可以和主库的结构完全一样,也可以不一样,从库主要用来进行只读的查询操作。在数量上从库的个数也可以根据查询的规模进行扩展,在业务逻辑上,也可以根据专题从主库中划分出不同的从库。从库也可以实现成ReportingDatabase,根据查询的业务需求,从主库中抽取一些必要的数据生成一系列查询报表来存储。
这里写图片描述
使用ReportingDatabase的一些优点通常可以使得查询变得更加简单高效:
• ReportingDatabase的结构和数据表会针对常用的查询请求进行设计。
• ReportingDatabase数据库通常会去正规化,存储一些冗余而减少必要的Join等联合查询操作,使得查询简化和高效,一些在主数据库中用不到的数据信息,在ReportingDatabase可以不用存储。
• 可以对ReportingDatabase重构优化,而不用去改变操作数据库。
• 对ReportingDatabase数据库的查询不会给操作数据库带来任何压力。
• 可以针对不同的查询请求建立不同的ReportingDatabase库。
当然这也有一些缺点,比如从库数据的更新。如果使用SQLServer,本身也提供了一些如故障转移和复制机制来方便部署。

三. CQRS的优缺点

CQRS架构的优点:
1. CQ两端架构分离、相互不受束缚,各自独立设计、扩展
2. C端通常结合DDD,解决复杂的业务逻辑;
3. Q端轻量级查询,多种不同的查询视图通过订阅事件来更新
4. C端通过分布式消息队列水平扩展,天然支持削峰
5. EDA架构,整个系统各个部分松耦合,可扩展性好
6. 架构层面做到无并发,实现Command的高吞吐
7. 技术架构和业务代码完全分离,程序员不用关心技术问题,更方便的分工合作
CQRS架构的缺点:
1. 不是强一致性,而是面向最终一致性
2. 强依赖高性能可靠的分布式消息队列
3. 必须有强大可靠的CQRS框架,从头做起成本高、风险大
4. 最好结合Event Sourcing模式,否则CQ分离意义不大
5. 一些CQRS的最佳原则提高了开发人员的门槛

四. 适宜CQRS的场景

  1. 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突
  2. 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致
  3. 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况
  4. 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型
  5. 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统
  6. 需要和其他系统整合,特别是需要和事件溯源Event Sourcing进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

五. 不适宜CQRS的场景

  1. 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。
  2. 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。
  3. 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。

六. CQRS与Event Sourcing的关系

在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回。在操作(Command)方面,是通过发送Command实现,由CommandBus处理特定的Command,然后由Command将特定的Event发布到EventBus上,然后EventBus使用特定的Handler来处理事件,执行一些诸如,修改,删除,更新等操作。这里,所有与Command相关的操作都通过Event实现。这样我们可以通过记录Event来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing就是用来进行存储和管理事件的。

七. CQRS的架构实现

CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。
架构如下图:
这里写图片描述
上面图中包含有很多的概念,但本质是和第一张图是一样的,只不过在其基础上进行了扩展和延伸,先列举一下所涉及的概念:
• Command Bus(命令总线):图中没有,应该放在 Command Handler 之前,可以看作是 Command 发布者。
• Command Handler(命令处理器):处理来自 Command Bus 分发的请求,可以看作是 Command 订阅者、处理者。
• Event Bus(事件总线):一般在 Command Handler 完成之后,可以看作是 Event 发布者。
• Event Handler(事件处理器):处理来自 Event Bus 分发的请求,可以看作是 Event 订阅者、处理者。
• Event Store(事件存储):对应概念 Event Sourcing(事件溯源),可以用于事件回放处理,还原指定对象状态。

上面有些是 EDA(事件驱动架构)中的概念,首先抽离两个重要概念:Command(命令)和 Event(事件),Command 是一种命令的语气,它的效果就是对某种对象状态的修改,Command Bus 收集来自 UI 的 Command 命令,并根据具体命令分发给具体的 Command Handler 进行处理,这时候就会产生一些领域操作,并对相应的领域对象进行修改,Command Handler 只是修改操作,并不会涉及到修改之后的操作(比如保存、事件发布等),Command Handler 完成之后并不表示这个 Command 命令就此结束,它需要把接下来的操作交给 Event Bus(完成之后的操作),并分发给相应的 Event Handler 订阅者进行处理,一般是数据保存、事件存储等。关于 Event Handler 保存领域状态操作,其实说简单也简单,说复杂会很复杂,对于它的实现一般会采用异步的方式,也就是说领域状态的保存操作不会延时领域中的业务操作,数据的一致性使用 Unit of Work,具体的领域状态保存用 Repository 实现。梳理 Command 整个流程,你会发现一个关键词:状态(Status),Command Bus 接收来自 UI 的请求,分发给相应的 Command Handler 进行处理,在处理过程中,就会对领域对象进行修改操作,但它不会保存修改之后的状态信息,而是交给 Event Handler 进行保存状态信息。
和 Command 相比,Query 的处理流程就简单很多了,Query Service 接收来自 UI 的查询请求,这个查询处理可以用各种方式实现,你可以使用 ORM,也可以直接写 SQL 代码,反正是:怎么能提高性能,就怎么来!返回的结果类型一般是 DTO(数据传输对象),根据 UI 进行设计,可以减少不必要的数据传输。

八. 总结

CQRS是一种思想很简单清晰的设计模式,他通过在业务上分离操作和查询来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在CQRS中,所有的涉及到对DB的操作都是通过发送Command,然后特定的Command触发对应事件来完成操作,这个过程是异步的,并且所有涉及到对系统的变更行为都包含在具体的事件中,结合Eventing Source模式,可以记录下所有的事件,而不是以往的某一点的数据信息,这些信息可以作为系统的操作日志,可以来对系统进行回退或者重放。CQRS 模式在实现上有些复杂,很多地方比如AggregationRoot、Domain Object都涉及到DDD中的相关概念。

参考文献
浅谈命令查询职责分离(CQRS)模式

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_16681169/article/details/81592415
文章标签: CQRS
个人分类: 领域驱动设计
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭