明天就是大年三十了,今天在家有空,想集中整理一下CQRS架构的特色以及相比传统架构的优缺点分析。先提早祝你们猴年新春快乐、万事如意、身体健康!前端
最近几年,在DDD的领域,咱们常常会看到CQRS架构的概念。我我的也写了一个ENode框架,专门用来实现这个架构。CQRS架构自己的思想其实很是简单,就是读写分离。是一个很好理解的思想。就像咱们用MySQL数据库的主备,数据写到主,而后查询从备来查,主备数据的同步由MySQL数据库本身负责,这是一种数据库层面的读写分离。关于CQRS架构的介绍其实已经很是多了,你们能够自行百度或google。我今天主要想总结一下这个架构相对于传统架构(三层架构、DDD经典四层架构)在数据一致性、扩展性、可用性、伸缩性、性能这几个方面的异同,但愿能够总结出一些优势和缺点,为你们在作架构选型时提供参考。java
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架构所带来的好处。