当前位置: 首页 > 科技观察

DDDCQRS架构与传统架构优缺点对比_0

时间:2023-03-11 20:25:54 科技观察

近几年,在DDD领域,我们经常看到CQRS架构的概念。我个人也写了一个ENode框架,专门用来实现这个架构的。CQRS架构本身的思想其实很简单,就是读写分离。这是一个很好理解的想法。就像我们使用MySQL数据库的master和backup一样,数据写入master,然后从slave上查询查看。主备数据的同步是MySQL数据库自己负责的,是数据库层面的读写分离。关于CQRS架构的介绍其实有很多,大家可以自行百度或者google。今天主要想总结一下这种架构与传统架构(三层架构,DDD经典四层架构)在数据一致性、可扩展性、易用性、可扩展性、性能等方面的异同。希望能总结出一些优点。以及缺点,给大家在做架构选择的时候提供一个参考。前言由于CQRS架构本身只是一种读写分离的思想,所以有多种实现方式。比如数据存储没有分离,只是代码层面的读写分离,这也是CQRS的体现;那么数据存储的读写分离,C端负责数据存储,Q端负责数据查询,通过C端产生的Event来同步Q端的数据,也就是CQRS架构的实现。我今天讨论的CQRS架构就是指这个实现。另外很重要的一点,我们还会在C端介绍EventSourcing+InMemory这两种架构思路。我认为这两种思想和CQRS架构可以完美的结合起来,充分发挥CQRS架构的最大价值。数据一致性在传统架构中,数据一般是强一致性的。我们通常使用数据库事务来保证一次操作中的所有数据修改都在一个数据库事务中,从而保证数据的强一致性。在分布式场景下,我们还想要数据的强一致性,那就是使用分布式事务。但是众所周知,分布式事务的难度和成本是非常高的,使用分布式事务的系统吞吐量会比较低,系统的可用性也会比较低。因此,很多时候,我们会放弃数据的强一致性,而采用最终一致性;从CAP定理的角度来看,就是放弃一致性,选择可用性。CQRS架构完全遵循最终一致性的概念。这种架构基于一个重要的假设,即用户看到的数据总是旧的。对于一个由多个用户操作的系统,这种现象是很常见的。比如闪购场景,在你下单之前,你在界面上看到的商品数量可能是有货的,但是下单的时候,系统提示商品已售罄。其实只要我们仔细想想,也确实如此。因为我们在界面上看到的数据是从数据库中取出来的,一旦显示在界面上,就不会改变。但是很有可能是别人修改了数据库中的数据。这种现象在大多数系统尤其是高并发的WEB系统中尤为常见。因此,基于这个假设,我们知道即使我们的系统实现了数据强一致性,用户也很可能看到旧数据。因此,这为我们设计架构提供了一种新思路。能不能这样:我们只需要保证系统中基于所有增删改查操作的数据是唯一的,而查询到的数据不一定是唯一的。这自然会引出CQRS架构。C端数据保持一致,数据强一致;Q端的数据不需要唯一,可以通过C端的事件异步更新。因此,基于这个想法,我们开始思考如何具体实现CQ的两端。看到这里,可能你还有一个疑问,为什么C端的数据一定要被hack?这个其实很好理解,因为如果要修改数据,可能会有一些修改的业务规则判断。如果你依据的数据不完善,说明判断没有意义或者不准确,所以基于旧数据的修改是没有意义的。可扩展的传统架构,组件之间存在很强的依赖关系,都是对象之间的直接方法调用;而CQRS架构是事件驱动的思想;从微聚合根层面来看,传统架构是应用层通过过程代码协调多个聚合根,以事务的方式一次性完成整个业务操作。CQRS架构,基于Saga的思想,最终以事件驱动的方式实现了多个聚合根的交互。另外,CQRS架构的CQ端也通过事件异步同步数据,这也是事件驱动的一种体现。上升到架构层面,前者是SOA的思想,后者是EDA的思想。SOA是一个服务调用另一个服务来完成服务之间的交互,服务之间是紧耦合的;EDA是一个组件订阅另一个组件的事件消息,并根据事件信息更新组件自身的状态。因此,在EDA架构中,每个组件都是不依赖于其他组件的;组件之间只通过主题相关,耦合度很低。上面提到了两种架构的耦合。显然,低耦合的架构必须具有良好的可扩展性。因为SOA的思想,当我想增加一个新的功能时,需要修改原来的代码;比如原来的服务A调用了两个服务B和C,后来我们要调用另一个服务D,就需要改变服务A的逻辑;对于EDA架构,我们不需要更改现有代码。本来有两个订阅者B和C订阅了A产生的消息,现在我们只需要增加一个新的消息订阅者D即可。从CQRS的角度来看,还有一个很明显的例子,就是可扩展性Q端。假设我们原来的Q端只是使用数据库实现的,但是后来系统访问量增加,数据库更新太慢或者不能满足高并发查询,所以希望增加缓存来应对高并发查询。这对于CQRS架构来说很容易,我们只需要添加一个新的事件订阅者来更新缓存。应该说我们可以很方便的随时增加Q端的数据存储类型。数据库、缓存、搜索引擎、NoSQL、日志等等。我们可以根据自己的业务场景选择合适的Q端数据存储,达到快速查询的目的。这一切都是因为我们的C端记录了所有的模型变更事件。当我们要添加一个新的View存储时,我们可以根据这些事件获取View存储的最新状态。这种可扩展性在传统架构下是很难实现的。Availability可用性无论是传统架构还是CQRS架构,都可以实现高可用,只要我们让我们系统中的每个节点都没有单点即可。不过相比之下,我觉得CQRS架构在易用性上有更多的回避和选择余地。在传统的架构中,因为没有读写分离,所以更难将读写结合在一起来提高可用性。因为传统的架构,如果一个系统在高峰期写并发量很大,比如2W,读并发量也很大,比如10W。那么系统一定要优化,同时支持这种高并发的写入和查询,否则系统会在高峰的时候挂掉。这是基于同步调用思想的系统的缺点。没什么削峰填谷,瞬间把多余的请求存起来,但是不管遇到多少请求,系统都必须能够及时处理,否则会造成雪崩效应,造成系统瘫痪。但是一个系统不会一直处于高峰期,高峰期可能只有半小时或者一小时;但是为了保证系统在高峰期不挂掉,我们必须使用足够的硬件来支撑这个高峰期。大多数时候不需要这么高的硬件资源,所以会浪费资源。因此,我们说基于同步调用和SOA思想的系统实现成本是非常昂贵的。在CQRS架构下,因为CQRS架构是读写分离的,所以可用性相当于被隔离成两部分来考虑。我们只需要考虑C端如何解决写的可用性,Q端如何解决读的可用性。我觉得C端解决可用性更容易一些,因为C端是消息驱动的。当我们要修改任何数据时,我们会将Command发送到分布式消息队列,然后后端消费者处理Command->生成领域事件->持久化事件->发布事件到分布式消息队列->***事件处理Q端消费。此链接是消息驱动的。相比传统架构直接调用服务方法,可用性要高很多。因为即使我们处理命令的后端consumer暂时挂了,也不会影响前端controller发送命令,controller还是可用的。从这个角度来说,CQRS架构在数据修改方面更有用。但是你可能会说,如果分布式消息队列挂了怎么办?哦,是的,这确实是可能的。但是一般的分布式消息队列属于中间件,一般的中间件具有高可用性(支持集群和主备切换),所以相对于我们的应用来说,可用性要高很多。另外,由于命令先发送到分布式消息队列,因此可以充分发挥分布式消息队列的优势:异步、拉取模式、削峰填谷、基于队列的水平扩展。这些特性可以保证即使前端Controller在高峰时段瞬间发送大量命令,也不会导致后端应用处理命令挂掉,因为我们根据自己的消费能力拉取命令.这也是CQRSC端在可用性方面的优势。其实本质上就是分布式消息队列带来的优势。所以,从这里我们可以体会到EDA架构(事件驱动架构)是非常有价值的,而且这个架构也体现了我们现在流行的ReactiveProgramming(响应式编程)思想。那么对于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)将数据更新回数据库。一共涉及到2个数据库IO。然后是CQRS架构,CQ两端花费的时间肯定比传统架构要多,因为CQRS架构最多有3个数据库IO,1)持久化命令;2)持久化事件;3)根据事件更新读库。为什么说最多?因为持久化命令这个步骤不是必须的,所以有一种场景是不需要持久化命令的。CQRS架构中持久化命令的目的是做幂等处理,即我们要防止同一个命令被处理两次。在什么情况下你不需要持久化命令?即在创建聚合根的时候,可能不需要持久化命令,因为创建聚合根产生的事件版本号一直是1,所以我们在持久化事件重复时可以根据事件版本号来检测这个.所以,我们说,如果要使用CQRS架构,就必须接受CQ数据的最终一致性,因为如果以读库的更新来完成操作流程,那么在那个业务场景中花费的时间很可能是比传统建筑更长。许多。但是,如果我们以C端处理结束,CQRS架构可能会更快,因为C端可能只需要一次数据库IO。我觉得这里很重要。对于CQR??S架构,我们更关注C端处理完成所花费的时间;Q端处理慢一点也没关系,因为Q端只是给我们看数据的(最终一致性)。我们在选择CQRS架构的时候,必须要接受Q端数据更新有点延迟的缺点,否则不应该使用这种架构。所以希望大家在根据自己的业务场景进行架构选择的时候,一定要充分认识到这一点。另外,上面讲数据一致性的时候,提到了传统架构使用事务来保证数据的强一致性;如果事务越复杂,一次事务锁定的表就越多,锁是系统可扩展性的敌人;在CQRS架构中,一个命令只会修改一个聚合根。如果需要修改多个聚合根,可以通过Saga实现。这样就绕开了复杂事务的问题,通过最终一致性的思想实现了最大并行度和最小并发度,从而提高了系统整体的吞吐量。因此,总的来说,两种架构都可以克服性能瓶颈。并且只要克服了性能瓶颈,扩展性就不是问题(当然,这里我没有考虑数据丢失导致系统不可用的问题,这个问题是所有架构都无法避免的,唯一的解决办法就是数据冗余,而不是数据冗余)此处展开)。两者的瓶颈在于数据持久化,但由于传统架构的系统大多需要将数据存储在关系型数据库中,因此只能采用分库分表的方案。对于CQR??S架构,如果只关注C端的瓶颈,C端要保存的东西很简单,就是命令和事件;如果你相信一些成熟的NoSQL(我认为使用MongoDB之类的文档型数据库更适合存储命令和事件),并且你有足够的能力和经验来运维它们,你可以考虑使用NoSQL进行持久化。如果你觉得NoSQL不可靠或者你不能完全控制它,你可以使用关系型数据库。但是这样一来,你也得下功夫,比如你需要负责分库分表来保存命令和事件,因为命令和事件的数据量是非常大的。但是目前阿里云等部分云服务提供了DRDS这种直接支持分库分表的数据库存储方案,大大简化了存储命令和事件的成本。个人觉得还是会采用分库分表的方案。原因很简单:保证数据可靠、成熟、可控,支持这种只读数据的落地。表格也不难。所以,通过这个对比,我们知道,在传统架构中,我们必须使用分库分表(除非OceanBase在阿里高端可以使用);而CQRS架构可以给我们带来更多的选择。因为持久化命令和事件非常简单,是不可修改的只读数据,对kv存储友好。也可以选择文档型NoSQL,C端会一直添加新数据,不会修改或删除数据。***,是关于Q端的瓶颈。如果你的Q端也用关系型数据库,那和传统架构一样,想怎么优化就怎么优化。CQRS架构允许你使用其他架构来实现Q,所以优化的方法相对多一些。结论我认为传统架构和CQRS架构都是很好的架构。传统架构门槛低,懂的人多,而且大部分项目并发写入量和数据量都不大。所以应该说对于大部分项目来说,传统的架构是可以的。但是通过本文的分析,大家也知道了传统架构确实存在着一些不足,比如在扩展性、可用性、性能瓶颈的解决上,都比CQRS架构要弱。如果有其他意见,欢迎拍砖,交流才能进步,呵呵。因此,如果你的应用场景是高并发写入、高并发读取、大数据,想要在可扩展性、可用性、性能、扩展性等方面表现得更好,我觉得可以试试CQRS架构。但是还有一个问题。CQRS架构的门槛很高。我觉得没有成熟的框架支持是很难用的。据我所知,目前业界成熟的CQRS框架并不多。Java平台有axon框架和jdon框架;.NET平台和ENode框架都在朝这个方向努力。所以,我认为这也是几乎没有使用CQRS架构的成熟案例的原因之一。还有一个原因是,使用CQRS架构需要开发者对DDD有一定的了解,否则实践起来会比较困难,DDD本身在了解几年后也很难在实践中应用。还有一个原因就是CQRS架构的核心非常依赖高性能的分布式消息中间件,所以选择高性能的分布式消息中间件也是一个门槛(Java平台有RocketMQ),而.NET平台我个人A分布式消息队列EQueue专门开发出来的,呵呵。另外,如果没有成熟的CQRS框架的支持,编码复杂度会非常复杂,比如EventSourcing、消息重试、消息幂等处理、事件的顺序处理、并发控制等。这些问题不是那么容易解决的。而如果有框架的支持,框架就会帮我们解决这些纯技术问题。开发者只需要关注如何建模,实现领域模型,如何更新阅读库,如何实现查询。那么就有可能使用CQRS架构,因为它可能比传统架构开发更简单,并且可以获得CQRS架构带来的很多好处。