当前位置:网站首页>从单体架构迁移到 CQRS 后,我觉得 DDD 并不可怕
从单体架构迁移到 CQRS 后,我觉得 DDD 并不可怕
2022-08-04 23:21:00 【倾听铃的声】
软件设计是一个不断发展演进的过程。每个大型系统都是从小微系统开始的。当现有的架构遇到问题而又无法解决时,系统就会开始演进。每一次演进都会伴随着一些技术上的选择。需要解决什么问题?需要付出什么样的代价?作为一名架构师或高级工程师,必须找到一种合理的演进方式,在开发进度、技术栈、团队水平等各方面都能满足条件,这样才能制定出可行的解决方案。
本文将介绍 CQRS(命令查询职责分离)的基本理念和要解决的问题。我们将从一个小型单体架构开始,逐步演进,像每一个软件系统的演进一样。本文将介绍每一次演进背后的原因和方法。
传统单体架构
这是最常见的系统设计。有一台 API 服务器,通常是 restful API,和一个数据库。客户端事先与后端协商好传输格式。读和写都是通过 DTO,即数据传输对象完成的。然而,后端在处理业务逻辑时需要将 DTO 转换为具有领域知识的领域对象,并使用领域对象作为数据库的存储单元。
为了实现读 / 写分离,在左边的写路径中,客户端向后端发送 DTO,对数据库进行 CUD(创建 / 更新 / 删除)操作,后端在处理完成后向客户端返回表示成功的 Ack 或表示失败的 Nak。通常,在 restful API 中,2xx 表示成功,4xx 表示失败。右边的读路径只是通过读请求来获得相应的 DTO。
再从客户端的的角度来说下 DTO 的含义。在客户端,DTO 通常包含要在屏幕上呈现的所有数据。例如,当你在社交媒体上查看自己的个人资料时,它将包括你的名字、账户和其他个人信息,以及你自己最近的活动,甚至你关注的活动。DTO 包含所有需要在这个页面上呈现的信息。
为什么我们要强调读 / 写分离?我们不能在读 / 写路径上使用同一个程序吗?因为我们想在将来更好地优化我们的系统。写路径有特定的优化方法,读路径也是如此。比如说,做一个缓存,在读路径上可以使用预读缓存来减少响应时间。而且,写路径可以通过写入缓存来优化。其次,也可以把写入操作异步执行。将所有 DTO 写入消息队列中,并由工作者进程负责处理,通过这种方式来处理大量的数据写入。此外,可以使用适当的数据库进行写入和读取。
因此,读 / 写分离是必不可少的。而且,在系统设计的早期阶段就应该考虑到这一点。写路径专注于数据的持久化;而读路径则专注于数据的查询。
然而,这个系统设计模型有两个主要问题:
贫血模型,也被称为 CRUD 模型。后端专注于数据转换而不处理业务逻辑,这将导致业务逻辑散落在各处,领域知识也会消失。例如,对于一个电子商务网站,我们会说“购买”,而不是“插入一条订单记录”。
可扩展性不足。从系统架构的角度来看,数据库很容易成为整个系统的瓶颈。读取和写入都必须在它上面进行。因为缺少横向扩展能力,RDBMS 的问题就更加严重了。
基于任务的单体架构
为了解决上述传统单体架构中存在的问题,这里我们尝试引入域的概念。
这个图与上面的图基本相同。唯一的区别是在写路径上用消息代替了 DTO。消息包含动作和数据,而不是像 DTO 那样只包含数据本身。因此,我们可以在消息中携带特定域的动作,使后端更容易识别每个动作,并有一个相应的域实现。
在这个阶段,CQRS 中的 C 出现了,消息就是一种命令。然而,可扩展性问题仍未得到解决。
另外,虽然我们简化了 DTO,改为使用消息进行通信,但在读路径上我们仍然需要 DTO。还是以社交媒体为例。在修改昵称时,消息的格式可能是{"rename": "LazyDr"}。但是当呈现个人资料时,我们还需要额外的信息,如活动。这种信息缺口使得我们有必要在读路径上做大量的处理来获取 DTO。
CQS(命令查询分离)
CQS 的出现就是为了解决以上读写分离的痛点。
读取时,客户端需要 DTO,所以后端可以在读路径上做一些专门针对读取的优化,比如从原来的域对象预先生成 DTO,并将 DTO 存储在专门的数据库中以供读取。
这样一来,在读路径上,应用服务的实现变得更加简单。应用服务会成为一个很薄的读取层,只负责分页、排序等工作。发出请求后,客户端很容易从数据库中检索到 DTO。
那么问题来了,谁来生成这些预建的 DTO 呢?这是写路径的职责。
虽然这幅图与之前看到的例子类似,但实际上,除了持久化域对象,应用服务还必须持久化 DTO。换句话说,大部分的业务逻辑都压在了写路径上,它还需要准备各种读视图。
至此,我们已经解决了遇到的大部分问题,但扩展性问题仍然没有得到解决。现在,我们进一步明确下扩展性,主要包括两个方面:流量:写入量增加。扩展:功能需求增加,例如需要各种不同的读视图。
继续以社交媒体为例,它有一个个人资料的展示,但可能有另一个按照时间线的展示。CQRS 为什么写路径要负责准备读视图?写应该专注于持久化,各种读视图不应该在写路径上处理。但是,读路径上只有读,谁该准备那些读视图?
因此,完整的解决方案是这样的:
左边的写路径和右边的读路径已经在 CQS 部分介绍过了。唯一的区别是增加了 Eventually,负责将写路径使用的数据库转换为读路径使用的数据库。一旦涉及到数据同步,就可能遇到数据一致性问题,所以这里列出了几种实现最终一致性的方法,按耗时从短到长排序如下:
后台线程:典型代表是 Redis。在数据写入主节点后,Redis 会立即在后台将数据发送到的副本中。
消息队列加工作者。这是异步数据复制的一种常见做法。在写入数据库时,会创建一个事件并发送到消息队列,然后由工作者处理。
提取 - 转换 - 加载:这个时间间隔最长,从几分钟到几小时不等。使用 map-reduce 或其他方法将结果写到另一边。
无论采用哪种方法,单一真相来源都是必须的。也就是说,如果在转换过程中发生任何故障,系统必须能够恢复未完成的工作。因此,数据必须唯一而且可靠。
通常,数据有两种类型:
状态:状态指你此刻看到的东西,比如说写在银行存折上的余额。
事件:事件是修改每个状态的动作,例如银行存折上的每一条交易记录。
实际上,我们已经有了可以作为事件存储的消息。对于写路径,按顺序存储消息非常有效。借助这些消息,很容易根据需要创建出不同的读视图。这种方法也被称为事件源。
但仅有事件还很难有效地利用。为了获得最终结果,每一次转换都必须从头到尾运行,以重建读视图。因此,最好是采用一种混合方法。在写路径上,将状态和事件都保留,转换过程可以根据实际情况选择数据源。
总结一下 CQRS 中数据的整个生命周期:
数据从客户端开始,以命令格式进入后端。根据业务逻辑,它被转换为域对象并存储在数据库中。这些域对象被转换为各种读视图,并根据要求存储在不同的专用读数据库中。最后,客户端以 DTO 的形式获取这些读视图。
小结
有许多书籍和文章以各种方式介绍了 DDD 和 CQRS。在我看来,这些模式限制了我们在进行 DDD 设计时的想象力,如实体、价值对象、聚合等。这使得大多数开发人员觉得,DDD 离自己很远,很难实现,也很难实施。事实上,DDD 的概念并不复杂;相反,DDD 是为了封装业务逻辑,促进功能需求的扩展。
CQRS 就更简单了。在这篇文章中,我们从系统演进的过程出发,介绍了整个系统的设计过程和需要解决的问题,最后自然地得出 CQRS 的结论。
系统设计中没有银弹。每一次演进都是为了解决一些特定的问题。然而,它可能会带来新的问题。以本文的设计过程为例,CQRS 似乎解决了所有提到的问题,“贫血模型”和可扩展性不足,但也带来了新的问题,如数据一致性。每一种技术选择都有它的权衡,只要了解每个选项背后的所有威胁因素,就可以选出相对可以接受的方法。
即使你选择了 CQRS,在实践中,实现最终的一致性仍然有三种方法可以选择。系统设计是不断选择的结果。
这篇文章的目的是告诉你,DDD 没有那么可怕,CQRS 也没有那么复杂,只是一个决定而已。
边栏推荐
猜你喜欢
应用联合、体系化推进。集团型化工企业数字化转型路径
C5750X7R2E105K230KA(电容器)MSP430F5249IRGCR微控制器资料
uniapp 分享功能-分享给朋友群聊朋友圈效果(整理)
MySQL基础篇【子查询】
Pytest学习-Fixture
MySQL的JSON 数据类型2
小黑leetcode之旅:95. 至少有 K 个重复字符的最长子串
Literature reading ten - Detect Rumors on Twitter by Promoting Information Campaigns with Generative Adversarial Learn
中国的顶级黑客在国际上是一个什么样的水平?
零基础如何入门软件测试?再到测开(小编心得)
随机推荐
Will we still need browsers in the future?(feat. Maple words Maple language)
Shell编程之循环语句与函数的使用
学生管理系统架构设计
956. 最高的广告牌
【SSR服务端渲染+CSR客户端渲染+post请求+get请求+总结】
typeScript-promise
OpenCV:10特征检测
kernel hung_task死锁检测机制原理实现
[Cultivation of internal skills of string functions] strncpy + strncat + strncmp (2)
【无标题】
亿流量大考(3):不加机器,如何抗住每天百亿级高并发流量?
NebulaGraph v3.2.0 Release Note, many optimizations such as the performance of querying the shortest path
一点点读懂cpufreq(二)
Basic web in PLSQL
线性DP(下)
uniapp 分享功能-分享给朋友群聊朋友圈效果(整理)
[QNX Hypervisor 2.2用户手册]10.5 vdev ioapic
MySQL的JSON 数据类型2
深度|医疗行业勒索病毒防治解决方案
Acwing3593. 统计单词