DDD CQRS 架构和传统架构的优缺点比较 - .net技术精选 - 开发者头条

标签: | 发表时间:2022-01-03 16:55 | 作者:
出处:https://toutiao.io

最近几年,在DDD的领域,我们经常会看到CQRS架构的概念。我个人也写了一个ENode框架,专门用来实现这个架构。CQRS架构本身的思想其实非常简单,就是读写分离。是一个很好理解的思想。就像我们用MySQL数据库的主备,数据写到主,然后查询从备来查,主备数据的同步由MySQL数据库自己负责,这是一种数据库层面的读写分离。关于CQRS架构的介绍其实已经非常多了,大家可以自行百度或google。我今天主要想总结一下这个架构相对于传统架构(三层架构、DDD经典四层架构)在数据一致性、扩展性、可用性、伸缩性、性能这几个方面的异同,希望可以总结出一些优点和缺点,为大家在做架构选型时提供参考。

前言

CQRS架构由于本身只是一个读写分离的思想,实现方式多种多样。比如数据存储不分离,仅仅只是代码层面读写分离,也是CQRS的体现;然后数据存储的读写分离,C端负责数据存储,Q端负责数据查询,Q端的数据通过C端产生的Event来同步,这种也是CQRS架构的一种实现。今天我讨论的CQRS架构就是指这种实现。另外很重要的一点,C端我们还会引入Event Sourcing+In Memory这两种架构思想,我认为这两种思想和CQRS架构可以完美的结合,发挥CQRS这个架构的最大价值。

数据一致性

传统架构,数据一般是强一致性的,我们通常会使用数据库事务保证一次操作的所有数据修改都在一个数据库事务里,从而保证了数据的强一致性。在分布式的场景,我们也同样希望数据的强一致性,就是使用分布式事务。但是众所周知,分布式事务的难度、成本是非常高的,而且采用分布式事务的系统的吞吐量都会比较低,系统的可用性也会比较低。所以,很多时候,我们也会放弃数据的强一致性,而采用最终一致性;从CAP定理的角度来说,就是放弃一致性,选择可用性。

CQRS架构,则完全秉持最终一致性的理念。这种架构基于一个很重要的假设,就是用户看到的数据总是旧的。对于一个多用户操作的系统,这种现象很普遍。比如秒杀的场景,当你下单前,也许界面上你看到的商品数量是有的,但是当你下单的时候,系统提示商品卖完了。其实我们只要仔细想想,也确实如此。因为我们在界面上看到的数据是从数据库取出来的,一旦显示到界面上,就不会变了。但是很可能其他人已经修改了数据库中的数据。这种现象在大部分系统中,尤其是高并发的WEB系统,尤其常见。

所以,基于这样的假设,我们知道,即便我们的系统做到了数据的强一致性,用户还是很可能会看到旧的数据。所以,这就给我们设计架构提供了一个新的思路。我们能否这样做:我们只需要确保系统的一切添加、删除、修改操作所基于的数据是最新的,而查询的数据不必是最新的。这样就很自然的引出了CQRS架构了。C端数据保持最新、做到数据强一致;Q端数据不必最新,通过C端的事件异步更新即可。所以,基于这个思路,我们开始思考,如何具体的去实现CQ两端。看到这里,也许你还有一个疑问,就是为何C端的数据是必须要最新的?这个其实很容易理解,因为你要修改数据,那你可能会有一些修改的业务规则判断,如果你基于的数据不是最新的,那意味着判断就失去意义或者说不准确,所以基于老的数据所做的修改是没有意义的。

扩展性

传统架构,各个组件之间是强依赖,都是对象之间直接方法调用;而CQRS架构,则是事件驱动的思想;从微观的聚合根层面,传统架构是应用层通过过程式的代码协调多个聚合根一次性以事务的方式完成整个业务操作。而CQRS架构,则是以Saga的思想,通过事件驱动的方式,最终实现多个聚合根的交互。另外,CQRS架构的CQ两端也是通过事件的方式异步进行数据同步,也是事件驱动的一种体现。上升到架构层面,那前者就是SOA的思想,后者是EDA的思想。SOA是一个服务调用另一个服务完成服务之间的交互,服务之间紧耦合;EDA是一个组件订阅另一个组件的事件消息,根据事件信息更新组件自己的状态,所以EDA架构,每个组件都不会依赖其他的组件;组件之间仅仅通过topic产生关联,耦合性非常低。

上面说了两种架构的耦合性,显而易见,耦合性低的架构,扩展性必然好。因为SOA的思路,当我要加一个新功能时,需要修改原来的代码;比如原来A服务调用了B,C两个服务,后来我们想多调用一个服务D,则需要改A服务的逻辑;而EDA架构,我们不需要动现有的代码,原来有B,C两订阅者订阅A产生的消息,现在只需要增加一个新的消息订阅者D即可。

从CQRS的角度来说,也有一个非常明显的例子,就是Q端的扩展性。假设我们原来Q端只是使用数据库实现的,但是后来系统的访问量增大,数据库的更新太慢或者满足不了高并发的查询了,所以我们希望增加缓存来应对高并发的查询。那对CQRS架构来说很容易,我们只需要增加一个新的事件订阅者,用来更新缓存即可。应该说,我们可以随时方便的增加Q端的数据存储类型。数据库、缓存、搜索引擎、NoSQL、日志,等等。我们可以根据自己的业务场景,选择合适的Q端数据存储,实现快速查询的目的。这一切都归功于我们C端记录了所有模型变化的事件,当我们要增加一种新的View存储时,可以根据这些事件得到View存储的最新状态。这种扩展性在传统架构下是很难做到的。

可用性

可用性,无论是传统架构还是CQRS架构,都可以做到高可用,只要我们做到让我们的系统中每个节点都无单点即可。但是,相比之下,我觉得CQRS架构在可用性方面,我们可以有更多的回避余地和选择空间。

传统架构,因为读写没有分离,所以可用性要把读写合在一起综合考虑,难度会比较更大。因为传统架构,如果一个系统的高峰期的并发写入很大,比如为2W,并发读取也很大,比如为10W。那该系统必须优化到能同时支持这种高并发的写入和查询,否则系统就会在高峰时挂掉。这个就是基于同步调用思路的系统的缺点,没有一个东西去削峰填谷,保存瞬间多出来的请求,而必须让系统不管遇到多少请求,都必须能及时处理完,否则就会造成雪崩效应,造成系统瘫痪。但是一个系统,不会一直处在高峰,高峰可能只有半小时或1小时;但为了确保高峰时系统不挂掉,我们必须使用足够的硬件去支撑这个高峰。而大部分时候,都不需要这么高的硬件资源,所以会造成资源的浪费。所以,我们说基于同步调用、SOA思想的系统的实现成本是非常昂贵的。

而在CQRS架构下,因为CQRS架构把读和写分离了,所以可用性相当于被隔离在了两个部分去考虑。我们只需要考虑C端如何解决写的可用性,Q端如何解决读的可用性即可。C端解决可用性,我觉得是更加容易的,因为C端是消息驱动的。我们要做任何数据修改时,都会发送Command到分布式消息队列,然后后端消费者处理Command->产生领域事件->持久化事件->发布事件到分布式消息队列->最后事件被Q端消费。这个链路是消息驱动的。相比传统架构的直接服务方法调用,可用性要高很多。因为就算我们处理Command的后端消费者暂时挂了,也不会影响前端Controller发送Command,Controller依然可用。从这个角度来说,CQRS架构在数据修改上可用性要更高。不过你可能会说,要是分布式消息队列挂了呢?呵呵,对,这确实也是有可能的。但是一般分布式消息队列属于中间件,一般中间件都具有很高的可用性(支持集群和主备切换),所以相比我们的应用来说,可用性要高很多。另外,因为命令是先发送到分布式消息队列,这样就能充分利用分布式消息队列的优势:异步化、拉模式、削峰填谷、基于队列的水平扩展。这些特性可以保证即便前端Controller在高峰时瞬间发送大量的Command过来,也不会导致后端处理Command的应用挂掉,因为我们是根据自己的消费能力拉取Command。这点也是CQRS C端在可用性方面的优势,其实本质也是分布式消息队列带来的优势。所以,从这里我们可以体会到EDA架构(事件驱动架构)是非常有价值的,这个架构也体现了我们目前比较流行的Reactive Programming(响应式编程)的思想。

然后,对于Q端,应该说和传统架构没什么区别,因为都是要处理高并发的查询。这点以前怎么优化的,现在还是怎么优化。但是就像我上面可扩展性里强调的,CQRS架构可以更方便的提供更多的View存储,数据库、缓存、搜索引擎、NoSQL,而且这些存储的更新完全可以并行进行,互相不会拖累。理想的场景,我觉得应该是,如果你的应用要实现全文索引这种复杂查询,那可以在Q端使用搜索引擎,比如ElasticSearch;如果你的查询场景可以通过keyvalue这种数据结构满足,那我们可以在Q端使用Redis这种NoSql分布式缓存。总之,我认为CQRS架构,我们解决查询问题会比传统架构更加容易,因为我们选择更多了。但是你可能会说,我的场景只能用关系型数据库解决,且查询的并发也是非常高。那没办法了,唯一的办法就是分散查询IO,我们对数据库做分库分表,以及对数据库做一主多备,查询走备机。这点上,解决思路就是和传统架构一样了。

性能、伸缩性

本来想把性能和伸缩性分开写的,但是想想这两个其实有一定的关联,所以决定放在一起写。

伸缩性的意思是,当一个系统,在100人访问时, 性能(吞吐量、响应时间)很不错,在100W人访问时性能也同样不错,这就是伸缩性。100人访问和100W人访问,对系统的压力显然是不同的。如果我们的系统,在架构上,能够做到通过简单的增加机器,就能提高系统的服务能力,那我们就可以说这种架构的伸缩性很强。那我们来想想传统架构和CQRS架构在性能和伸缩性上面的表现。

说到性能,大家一般会先思考一个系统的性能瓶颈在哪里。只要我们解决了性能瓶颈,那系统就意味着具有通过水平扩展来达到可伸缩的目的了(当然这里没有考虑数据存储的水平扩展)。所以,我们只要分析一下传统架构和CQRS架构的瓶颈点在哪里即可。

传统架构,瓶颈通常在底层数据库。然后我们一般的做法是,对于读:通常使用缓存就可以解决大部分查询问题;对于写:办法也有很多,比如分库分表,或者使用NoSQL,等等。比如阿里大量采用分库分表的方案,而且未来应该会全部使用高大上的OceanBase来替代分库分表的方案。通过分库分表,本来一台数据库服务器高峰时可能要承受10W的高并发写,如果我们把数据放到十台数据库服务器上,那每台机器只需要承担1W的写,相对于要承受10W的写,现在写1W就显得轻松很多了。所以,应该说数据存储对传统架构来说,也早已不再是瓶颈了。

传统架构一次数据修改的步骤是:1)从DB取出数据到内存;2)内存修改数据;3)更新数据回DB。总共涉及到2次数据库IO。

然后CQRS架构,CQ两端加起来所用的时间肯定比传统架构要多,因为CQRS架构最多有3次数据库IO,1)持久化命令;2)持久化事件;3)根据事件更新读库。为什么说最多?因为持久化命令这一步不是必须的,有一种场景是不需要持久化命令的。CQRS架构中持久化命令的目的是为了做幂等处理,即我们要防止同一个命令被处理两次。那哪一种场景下可以不需要持久化命令呢?就是当命令时在创建聚合根时,可以不需要持久化命令,因为创建聚合根所产生的事件的版本号总是为1,所以我们在持久化事件时根据事件版本号就能检测到这种重复。

所以,我们说,你要用CQRS架构,就必须要接受CQ数据的最终一致性,因为如果你以读库的更新完成为操作处理完成的话,那一次业务场景所用的时间很可能比传统架构要多。但是,如果我们以C端的处理为结束的话,则CQRS架构可能要快,因为C端可能只需要一次数据库IO。我觉得这里有一点很重要,对于CQRS架构,我们更加关注C端处理完成所用的时间;而Q端的处理稍微慢一点没关系,因为Q端只是供我们查看数据用的(最终一致性)。我们选择CQRS架构,就必须要接受Q端数据更新有一点点延迟的缺点,否则就不应该使用这种架构。所以,希望大家在根据你的业务场景做架构选型时一定要充分认识到这一点。

另外,上面再谈到数据一致性时提到,传统架构会使用事务来保证数据的强一致性;如果事务越复杂,那一次事务锁的表就越多,锁是系统伸缩性的大敌;而CQRS架构,一个命令只会修改一个聚合根,如果要修改多个聚合根,则通过Saga来实现。从而绕过了复杂事务的问题,通过最终一致性的思路做到了最大的并行和最少的并发,从而整体上提高系统的吞吐能力。

所以,总体来说,性能瓶颈方面,两种架构都能克服。而只要克服了性能瓶颈,那伸缩性就不是问题了(当然,这里我没有考虑数据丢失而带来的系统不可用的问题。这个问题是所有架构都无法回避的问题,唯一的解决办法就是数据冗余,这里不做展开了)。两者的瓶颈都在数据的持久化上,但是传统的架构因为大部分系统都是要存储数据到关系型数据库,所以只能自己采用分库分表的方案。而CQRS架构,如果我们只关注C端的瓶颈,由于C端要保存的东西很简单,就是命令和事件;如果你信的过一些成熟的NoSQL(我觉得使用文档性数据库如MongoDB这种比较适合存储命令和事件),且你也有足够的能力和经验去运维它们,那可以考虑使用NoSQL来持久化。如果你觉得NoSQL靠不住或者没办法完全掌控,那可以使用关系型数据库。但这样你也要付出努力,比如需要自己负责分库分表来保存命令和事件,因为命令和事件的数据量都是很大的。不过目前一些云服务如阿里云,已经提供了DRDS这种直接支持分库分表的数据库存储方案,极大的简化了我们存储命令和事件的成本。就我个人而言,我觉得我还是会采用分库分表的方案,原因很简单:确保数据可靠落地、成熟、可控,而且支持这种只读数据的落地,框架内置要支持分库分表也不是什么难事。所以,通过这个对比我们知道传统架构,我们必须使用分库分表(除非阿里这种高大上可以使用OceanBase);而CQRS架构,可以带给我们更多选择空间。因为持久化命令和事件是很简单的,它们都是不可修改的只读数据,且对kv存储友好,也可以选择文档型NoSQL,C端永远是新增数据,而没有修改或删除数据。最后,就是关于Q端的瓶颈,如果你Q端也是使用关系型数据库,那和传统架构一样,该怎么优化就怎么优化。而CQRS架构允许你使用其他的架构来实现Q,所以优化手段相对更多。

结束语

我觉得不论是传统架构还是CQRS架构,都是不错的架构。传统架构门槛低,懂的人也多,且因为大部分项目都没有什么大的并发写入量和数据量。所以应该说大部分项目,采用传统架构就OK了。但是通过本文的分析,大家也知道了,传统架构确实也有一些缺点,比如在扩展性、可用性、性能瓶颈的解决方案上,都比CQRS架构要弱一点。大家有其他意见,欢迎拍砖,交流才能进步,呵呵。所以,如果你的应用场景是高并发写、高并发读、大数据,且希望在扩展性、可用性、性能、可伸缩性上表现更优秀,我觉得可以尝试CQRS架构。但是还有一个问题,CQRS架构的门槛很高,我认为如果没有成熟的框架支持,很难使用。而目前据我了解,业界还没有很多成熟的CQRS框架,java平台有axon framework, jdon framework;.NET平台,ENode框架正在朝这个方向努力。所以,我想这也是为什么目前几乎没有使用CQRS架构的成熟案例的原因之一。另一个原因是使用CQRS架构,需要开发者对DDD有一定的了解,否则也很难实践,而DDD本身要理解没个几年也很难运用到实际。还有一个原因,CQRS架构的核心是非常依赖于高性能的分布式消息中间件,所以要选型一个高性能的分布式消息中间件也是一个门槛(java平台有RocketMQ),.NET平台我个人专门开发了一个分布式消息队列EQueue,呵呵。另外,如果没有成熟的CQRS框架的支持,那编码复杂度也会很复杂,比如Event Sourcing,消息重试,消息幂等处理,事件的顺序处理,并发控制,这些问题都不是那么容易搞定的。而如果有框架支持,由框架来帮我们搞定这些纯技术问题,开发人员只需要关注如何建模,实现领域模型,如何更新读库,如何实现查询,那使用CQRS架构才有可能,因为这样才可能比传统的架构开发更简单,且能获得很多CQRS架构所带来的好处。

相关 [ddd cqrs 架构] 推荐:

DDD CQRS 架构和传统架构的优缺点比较 - .net技术精选 - 开发者头条

- -
最近几年,在DDD的领域,我们经常会看到CQRS架构的概念. 我个人也写了一个ENode框架,专门用来实现这个架构. CQRS架构本身的思想其实非常简单,就是读写分离. 就像我们用MySQL数据库的主备,数据写到主,然后查询从备来查,主备数据的同步由MySQL数据库自己负责,这是一种数据库层面的读写分离.

Axon Saga的使用 | event sourcing | CQRS | axon | DDD | EdisonXu的技术分享

- -
在上一篇里面,我们正式的使用了CQRS模式完成了AXON的第一个真正的例子,但是细心的朋友会发现一个问题,创建订单时并没有检查商品库存. 库存是否足够直接回导致订单状态的成功与否,在并发时可能还会出现超卖. 当库存不足时还需要回滚订单,所以这里出现了复杂的跨Aggregate事务问题. Saga就是为解决这里复杂流程而生的.

对CQRS/EventSourcing架构的思考

- -
开始之前想先说一下微服务架构和CQRS架构的区别和联系. 微服务架构现在很热,到处可以看到各大互联网公司的微服务实践的分享总结. 但是,我今天的分享和微服务没有关系,希望可以带给大家一些新的东西. 如果一定要说微服务和CQRS架构的关系,那我觉得微服务是一种边界思维,微服务的目的是为了从业务角度拆分(职责分离)当前业务领域的不同业务模块到不同的服务,每个微服务之间的数据完全独立,它们之间的交互可以通过SOA.

基于DDD的微服务架构设计

- - ITeye博客
DDD领域驱动设计(DDD:Domain-Driven Design).     现有的架构设计实在受不了,业务的反反复复地变化,导致代码圈复杂度之深让人恐惧. 之前的微服务架构经验让我更加彻底点,采用DDD领域驱动设计进行整个改变.     随着经过几个月的努力,确实慢慢地体会到ddd的架构设计的优势,聚合根设计能够协助我们整个服务改造,开发起来越来越迅速.

使用Docker+springboot+springcloud+axon 构建CQRS/ES的微服务架构_quguang65265的博客-CSDN博客

- -
在过去几年中,软件架构的变化步伐迅速发展. DevOps,Microservices和Containerisation等新方法已成为热门话题,采用率迅速提高. 在这篇文章中,我想向您介绍一个我一直在研究的微服务项目,它结合了过去几年中两个突出的架构进步:命令和查询责任分离(CQRS)和容器化. 在第一部分中,我将向您展示使用容器分发和运行多服务器微服务应用程序是多么容易.

CQRS基本概念 | event sourcing | CQRS | axon | EdisonXu的技术分享

- -
在研究微服务的过程中,跨服务的操作处理,尤其是带有事务性需要统一commit或rollback的,是比较麻烦的. 本系列记录了我在研究这一过程中的心得体会. 本篇主要就以下几个问题进行介绍:. 什么是EventSourcing. EventSourcing和CQRS的关系. CQRS/ES怎么解决微服务的难题.

CQRS——命令与查询职责分离【翻译】

- - 企业架构 - ITeye博客
翻译自大牛Martin Fowler的CQRS. 原文地址:http://martinfowler.com/bliki/CQRS.html. CQRS代表着命令查询责任分离. 我第一次听到这个模式是从Greg Young那里. 这个模式的核心就是你在更新数据时所使用的模型可以与读取和查询时所用的数据模型不一样.

为什么在做微服务设计的时候需要DDD?

- - DockOne.io
记得之前在规划和设计微服务架构的时候,张队长给了我一个至今依然记忆深刻的提示:『你的设计蓝图里为什么没有看到DDD的影子呢. 随着对充血模型的领域认知的加深,我越加感觉到DDD的重要性. 但是DDD内容繁多,是不是要深入去了解呢,我觉得不必入坑太深,个人浅见,它最核心的一点就是针对贫血模型的不足而设计,把原先传统的贫血模型里的业务逻辑层拎出来,融入到Domain层,这样面对复杂业务的规模化变更,我们只需要专注于Domain即可.

结合电商支付业务一文搞懂 DDD

- - IT瘾-dev
作者范钢,曾任航天信息首席架构师,《大话重构》一书的作者. 本文结合电商支付场景详细描述了领域驱动模型的实际应用. 2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计:软件核心复杂性应对之道》面世,从书名可以看出,这是一本应对软件系统越来越复杂的方法论的图书. 然而,在当时,中国的软件业才刚刚起步,软件系统还没有那么复杂,即使维护了几年,软件退化了,不好维护了,推倒重新开发就好了.

用DDD指导微服务拆分 - ThoughtWorks洞见

- -
对于服务拆分的逻辑来说,是先设计高内聚低耦合的领域模型,再实现相应的分布式系统. 服务的划分有一些基本的方法和原则,通过这些方法能让微服务划分更有操作性. 最终在微服务落地实施时也能按图索骥,无论是对遗留系统改造还是全新系统的架构都能游刃有余. 开发者在刚开始尝试实现自己的微服务架构时,往往会产生一系列问题 :.