DDD CQRS架构和传统架构的优缺点比较

明天就是大年三十了,今天在家有空,想集中整理一下CQRS架构的特点以及相比传统架构的优缺点分析。先提前祝大家猴年新春快乐、万事如意、身体健康!

最近几年,在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。这点也是CQRSC端在可用性方面的优势,其实本质也是分布式消息队列带来的优势。所以,从这里我们可以体会到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架构,如果我们只关注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架构所带来的好处。

转载:http://www.jdon.com/47852

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图:提供一种方法顺序访问一个聚合对象中的每个元素,而又不想暴露该对象的内部表示。应用:STL标准库迭代器实现、Java集合类型迭代器等模式结构:心得:迭代器模式的目的是在不获知集合对象内部细节的同时能对集合元素进行遍历操作
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:(1)同步阻塞IO(BlockingIO):即传统的IO模型。(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的N
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定义一系列算法,把他们封装起来,并且使他们可以相互替换,使算法可以独立于使用它的客户而变化。应用:排序的比较方法、封装针对类的不同的算法、消除条件判断、寄存器分配算法等。模式结构:心得:对对象(Context)的处理操作可
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个作用于某对象结构中的各元素的操作,它使你在不改变各元素的类的前提下定义作用于这些元素的新操作。应用:作用于编译器语法树的语义分析算法。模式结构:心得:访问者模式是要解决对对象添加新的操作和功能时候,如何尽可能不修改对象的类的一种方
命令模式(Command)命令模式(Command)[Action/Transaction]意图:将一个请求封装为一个对象,从而可用不同的请求对客户参数化。对请求排队或记录请求日志,以及支持可撤消的操作。应用:用户操作日志、撤销恢复操作。模式结构:心得:命令对象的抽象接口(Command)提供的两个
生成器模式(Builder)生成器模式(Builder)意图:将一个对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。 应用:编译器词法分析器指导生成抽象语法树、构造迷宫等。模式结构:心得:和工厂模式不同的是,Builder模式需要详细的指导产品的生产。指导者(Director)使用C
设计模式学习心得《设计模式:可复用面向对象软件的基础》一书以更贴近读者思维的角度描述了GOF的23个设计模式。按照书中介绍的每个设计模式的内容,结合网上搜集的资料,我将对设计模式的学习心得总结出来。网络上关于设计模式的资料和文章汗牛充栋,有些文章对设计模式介绍生动形象。但是我相信“一千个读者,一千个
工厂方法模式(Factory Method)工厂方法模式(Factory Method)[Virtual Constructor]意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类,使一个类的实力化延迟到子类。应用:多文档应用管理不同类型的文档。模式结构:心得:面对同一继承体系(Produc
单例模式(Singleton)单例模式(Singleton)意图:保证一个类只有一个实例,并提供一个访问它的全局访问点。应用:Session或者控件的唯一示例等。模式结构:心得:单例模式应该是设计模式中最简单的结构了,它的目的很简单,就是保证自身的实例只有一份。实现这种目的的方式有很多,在Java中
装饰者模式(Decorator)装饰者模式(Decorator)[Wrapper]意图:动态的给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。应用:给GUI组件添加功能等。模式结构:心得:装饰器(Decorator)和被装饰的对象(ConcreteComponent)拥有统一的接口
抽象工厂模式(Abstract Factory)抽象工厂模式(Abstract Factory)[Kit]意图:提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。应用:用户界面工具包。模式结构:心得:工厂方法把生产产品的方式封装起来了,但是一个工厂只能生产一类对象,当一个工厂需要生
桥接模式(Bridge)桥接模式(Bridge)[Handle/Body]意图:将抽象部分与它的实现部分分离,使他们都可以独立的变化。应用:不同系统平台的Windows界面。模式结构:心得:用户所见类体系结构(Window派生)提供了一系列用户的高层操作的接口,但是这些接口的实现是基于具体的底层实现
适配器模式(Adapter)适配器模式(Adapter)[Wrapper]意图:将类的一个接口转换成用户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。应用:将图形类接口适配到用户界面组件类中。模式结构:心得:适配器模式一般应用在具有相似接口可复用的条件下。目标接口(Targ
组合模式(Composition)组合模式(Composition)意图:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。应用:组合图形、文件目录、GUI容器等。模式结构:心得: 用户(Client)通过抽象类(Component)提供的公用接口统一
原型模式(Prototype)原型模式(Prototype)意图:用原型实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象。应用:Java/C#中的Clonable和IClonable接口等。模式结构:心得:原型模式本质上就是对象的拷贝,使用对象拷贝代替对象创建的原因有很多。比如对象的初始化构
什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。