DDD与CQRS架构

一、CQRS架构溯源

1.1 CQS理论

    Bertrand Meyer大佬在其书中提出了命令查询分离的理论。这里的命令是对CURD中的增,删,改做了抽象,同时认为方法只会执行命令或者查询。另外一方面这种理论也跟命令模式有相似之处。

1.2 从CQS到CQRS

    CQRS将命令和查询进行职责分离,明确各自的能力和优势,因此在与命令,事件等进行融合的时候可以更容易的进行融合。Greg Young大佬的论文《CQRS Documents by Greg Young》为CQRS进行了相对完整的理论阐述。

    国内有大佬张弛对其做了完整的翻译,我专门花了一些时间读了很多次,也只能理解一部分,这里引用一下,全篇分为四篇内容,如下:

《传统架构》:https://blog.zhangchi.fun/2020/04/28/CQRS-A-Stereotypical-Architecture/

《基于任务的用户界面》:https://blog.zhangchi.fun/2020/04/29/CQRS-task-based-user-interface/

《命令和查询职责分离》:https://blog.zhangchi.fun/2020/06/03/CQRS-CQRS/

《事件作为存储机制》:https://blog.zhangchi.fun/2020/06/12/EventsAsAStorageMechanism/

二、如何使用CQRS架构

2.1 CQRS架构应用场景

    在读了很多关于CQRS的文章之后发现大多都是在阐述这些概念和一些场景,至于我们怎么用CQRS,在什么场景下使用没有明确的说明,另外一方面CQRS确实不是任何场景都适用的。这里我简单总结一些,大概可以用在哪些场景下:

1.读多写少的场景2.写多读少的场景3.读写分离的场景4.面向行为,领域的场景5.需要事件回溯的场景

2.2 CQRS与DDD的取舍

    很多大佬也在尝试将CQRS与DDD进行融合,无奈的是已经有大佬撞南墙了,而且得到了一些宝贵的经验,比如 Greg Young在其论文中提到的一些问题。但是这些问题并没有得到重复的验证,只是说理论上我们可以融合DDD的概念模式去掩盖CQRS对领域的冲击,下面我们将分析一下为什么融合这么困难。

2.2.0 CQRS的技术基因

    读了很多篇文章和论文翻译之后CQRS给我的直觉就是这个理论思想本质上是面向技术的,就是对当前系统,模型,模块做了技术优化得到的方法论。因此这个思想实际上是基于技术而延伸出来的。因此跟DDD的本质是不一样的。这里多说一句DDD的本质就是面向对象建模,各个行业领域都可以用这个理论去做。


    相应的,各个系统在本质上都是CURD,进而说白了就是命令和查询两种,CQRS只是CURD的升级版,所以两者融合则会出现偏差。在CQRS里强行进行DDD则会寸步难行,还不如直接命令和查询或者CURD来的快。

    从反面分析一下DDD并没有要求或者显示的对对象进行CURD,尽管到基础设施层肯定会涉及到这些。但是DDD更关注面向对象和领域的方方面面,当然这也并不代表着两者没有共同点,下面分析一下CQRS周边,命令端和查询端与DDD的异同点。

2.2.1 CQRS背后的术语

命令,查询,事件,CURD,阻抗失衡,职责分离,事件存储,事件流

2.2.2 CQRS的命令端与DDD

Greg Young大佬的论文《CQRS Documents by Greg Young》中提到传统领域驱动设计代码中存在的一些问题,如:

1.在仓储上进行的很多查询方法通常包含了分页或排序信息。2.为创建DTO,Get属性会公开领域内部的状态。3.查询数据时,有时需要预先查询出其他数据(比如为了查询某个值对象相关数据,需要先查询出这些值对象所属的聚合根)。4.加载多个聚合根以构建DTO会导致对数据模型的查询不是最优的。另外,由于DTO构建操作,可能会混淆聚合边界。

    文中所指明的这些领域驱动设计中的问题确实存在,因为无可避免的会被一些XXX管理系统,XXX运营后天这些业务需求所束缚。因此一种潜在的解决方法就是CQRS,简要的说下就是将查询独立出来。命令(增,删,改)这些则不一定会在领域中体现,但是会在基础设施层中体现,也就是说CQRS可以从侧面对领域驱动设计代码进行优化。

    因此当我们在领域驱动设计中,对增,删,改之类的操作就变成了命令式的或者是事件式的。但是要注意领域驱动关注的不仅仅是业务活动发生前也关注发生和之后。因此不可以强行将命令端融入领域驱动设计层中。

    那么我怎么才能将命令式的方式和事件式的方式用到领域驱动设计项目或者代码中呢?这里给出一个相对可行的方式,在COLA架构中应用层(app)的包模块包括:

clientimpl:服务对外透出的API实现包command:命令模型consumer:处理外部messageexecutor:处理request,包括command和queryquery:查询包

    也就是说CQRS的命令端和查询端都将在应用层中一起与clientimpl(类似dubbo对外暴露接口的实现层)进行交互,将对外暴露的接口操作转换成命令和查询,通过executor(可选)调用领域层接口。

    另外一种方式就是我不用command是不是也可以,直接调用领域服务不行吗?其实可以,但是在很多大佬实践的过程中加入命令确实会有一定作用和优势,其中一个就是可以通过命令解耦应用层和领域层,另外一个优势就是可以将命令式的接口操作与事件结合起来,毕竟在领域服务中可能需要发送事件,消费事件。对事件的消费放在应用层其实跟clientimpl有异曲同工之妙,就是我依然把消费事件当作接口操作调用领域服务层。

2.2.3 CQRS的查询端与DDD

    在查询端依然有上面提到的问题,也就是说我们需要针对查询动刀子了。怎么动是个问题,毕竟查询从应用层到领域层,到基础设施层再回来调用链路也长,数据转换,路径依赖也多,涉及到上下游联动。另外一方面,就是分离的话是我所有查询的操作都跟增删改彻底分离吗,所以需要区别对待。

    在诸如XXX管理系统中,查询列表是必有的一个功能,类似这种的可以分离开来,另外一方面当领域层中需要通过ID获取关联数据的时候,这里可以不用分离。也就是说我们可以针对相对复杂,没有太多领域特性的查询功能进行分离。

    我们通过COLA分包策略和4层架构来看,假设我有一个查询,类似博客查询,或者XXX订单查询的功能,中间会用到诸如缓存,NOSQL,MySQL之类的底层数据存储引擎。其中一种实现方案就是我不经过领域层,而是由应用层的query包直接调用基础设施层的Redis,es,dao进行数据组装,转换。第二种方式就是在基础设施层中的factory+repository对查询数据进行组装,为了避免增加repository+factory的复杂度,查询要单独建立factory类+repository类,类似读写分离。在聚合根上根据需求自由聚合。总结一下上述解决方案:

1.跨层调用2.基础设施层(factory+repository)读写分离

需要说明的是上述解决方案依然是有缺点的,大概缺点如下:

1.查询代码可能会有重复2.查询分离的度不容易把握3.通过查询看不到领域模型的完整性4.随着业务迭代查询代码可能无法适应变更,改动成本增加5.调用链路增加,service业务分散

三、CQRS架构融合

3.1 CQRS架构与事件架构融合

    将CQRS架构思想应用在实战的时候,很容易犯一个错误,就是将命令端与事件完全绑定,导致的一个情况就是将关注点转移到了对事件,命令的关注而不是领域业务的关注。因此到后期两者绑定越深问题越大。通过上面的分析其实我们可以将CQRS与DDD+事件架构进行轻度融合,比如在命令端其实也不一定有命令绝对执行,不一定有命令需要在队列里面持久化,不一定执行之后完全绑定事件,即使要绑定也是通过一系列领域操作之后再进行事件触发或者消费。


    另外一方面在基础设施层中肯定已经有了对MQ操作的封装,从命令到领域操作再到事件然后到消息体其实是分离的。因此不需要将一个新增操作的命令直接与新增XXX事件绑定,要看最终的执行结果再决定这个命令是否需要形成事件到消息进行触发。

3.2 CQRS架构与缓存架构融合

    在缓存架构中其实读写不是严格区分的,那么CQRS区分了之后怎么将两者结合到一起呢。通过调用关系可以知道其实CQRS会依赖缓存架构,这里的缓存可以是本地缓存,或者远程缓存或者多级缓存这种的方式,但是最终基础设施层会处理缓存需求。

    那么需要做的就是在命令端和查询端怎么调用缓存服务了,通过上面的描述其实命令端不会直接跟缓存服务打交道,而是伴随着领域操作顺便就调用了缓存服务了。在查询端的请求有可能在应用层通过基础设施层调用了缓存服务,也有可能在repository+factory触发的查询,那么这里就会涉及到数据一致性的问题。毕竟读分离了,在读的过程中数据有可能是新的也有可能是旧的。查询端不容易感知数据的变化。

    另外一个点就是在领域驱动设计中对象是有生命周期的,这里的一个表现就是对象默认会在内存中存在一段时间,我们常规的思路就是一个请求过来直接通过dao返回了,service里面是无状态无数据的,再来一条请求还是这样,从数据库返回的数据不会被共享或者再次使用。也就是说查询这块一旦按照领域驱动设计的方式处理对象就会出现数据一致性的问题。这里我想到大概有两种方式来尽可能解决这个问题。

1.通过锁

这里的锁会让查询端感知一下,假如有锁那么查询则需要阻塞一下,直到无锁。

2.通过版本号或者快照的方式

查询的请求会带有数据版本号,当版本号不对的时候重新获取新数据。

3.3 CQRS架构与事务融合

    在CQRS架构中查询被剥离之后看上去跟事务毫无关系,仔细分析一下其实还是有关系的,其中一个问题就是查询端的数据一致性问题,比如查询的时候涉及到的数据有事务怎么解决,命令端到事件消息到其他领域服务的分布式事务怎么解决。

    在进行事务融合的时候我们依然可以通过轻度应用CQRS的方式避免这个问题,由于命令端+查询端都放在应用层的包里面,因此不会到领域服务层。另外的查询分离也不会将repository包的全部查询接口都迁移出去。因此当有些查询接口在事务中时也不会影响到查询端。

    至于后续引发的分布式事务消息,数据一致性都有一些技术框架上的解决思路,所以CQRS与事务之间的关系其实类似于洋葱圈架构,命令端和查询端都在外延。

四、参考资源

https://www.raychase.net/259 

https://www.infoq.cn/news/from-cqs-to-cqrs 

https://www.cnblogs.com/netfocus/p/10861152.html

https://www.eventstore.com/blog/event-sourcing-and-cqrs

https://blog.csdn.net/xichenguan/article/details/78810555


原文始发于微信公众号(神帅的架构实战):DDD与CQRS架构

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

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241702.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!