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

发布时间:2026/7/3 1:27:49

明天就是大年三十了,今天在家有空,想集中整理一下CQRS架构的特点以及相比传统架构的优缺点分析。先提前祝大家猴年新春快乐、万事如意、身体健康! CQRS架构由于本身只是一个读写分离的思想实现方式多种多样。比如数据存储不分离仅仅只是代码层面读写分离也是CQRS的体现然后数据存储的读写分离C端负责数据存储Q端负责数据查询Q端的数据通过C端产生的Event来同步这种也是CQRS架构的一种实现。今天我讨论的CQRS架构就是指这种实现。另外很重要的一点C端我们还会引入Event SourcingIn 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发送CommandController依然可用。从这个角度来说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次数据库IO1持久化命令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存储友好也可以选择文档型NoSQLC端永远是新增数据而没有修改或删除数据。最后就是关于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

相关新闻