写点什么

从单体架构迁移到 CQRS 后,我觉得 DDD 并不可怕

  • 2022-07-22
  • 本文字数:3101 字

    阅读完需:约 10 分钟

从单体架构迁移到 CQRS 后,我觉得 DDD 并不可怕

本文最初发布于 InterviewNoodle 博客。


软件设计是一个不断发展演进的过程。每个大型系统都是从小微系统开始的。当现有的架构遇到问题而又无法解决时,系统就会开始演进。每一次演进都会伴随着一些技术上的选择。需要解决什么问题?需要付出什么样的代价?作为一名架构师或高级工程师,必须找到一种合理的演进方式,在开发进度、技术栈、团队水平等各方面都能满足条件,这样才能制定出可行的解决方案。


本文将介绍 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,负责将写路径使用的数据库转换为读路径使用的数据库。一旦涉及到数据同步,就可能遇到数据一致性问题,所以这里列出了几种实现最终一致性的方法,按耗时从短到长排序如下:


  1. 后台线程:典型代表是 Redis。在数据写入主节点后,Redis 会立即在后台将数据发送到的副本中。

  2. 消息队列加工作者。这是异步数据复制的一种常见做法。在写入数据库时,会创建一个事件并发送到消息队列,然后由工作者处理。

  3. 提取 - 转换 - 加载:这个时间间隔最长,从几分钟到几小时不等。使用 map-reduce 或其他方法将结果写到另一边。


无论采用哪种方法,单一真相来源都是必须的。也就是说,如果在转换过程中发生任何故障,系统必须能够恢复未完成的工作。因此,数据必须唯一而且可靠。


通常,数据有两种类型:


  1. 状态:状态指你此刻看到的东西,比如说写在银行存折上的余额。

  2. 事件:事件是修改每个状态的动作,例如银行存折上的每一条交易记录。


实际上,我们已经有了可以作为事件存储的消息。对于写路径,按顺序存储消息非常有效。借助这些消息,很容易根据需要创建出不同的读视图。这种方法也被称为事件源。


但仅有事件还很难有效地利用。为了获得最终结果,每一次转换都必须从头到尾运行,以重建读视图。因此,最好是采用一种混合方法。在写路径上,将状态和事件都保留,转换过程可以根据实际情况选择数据源。


总结一下 CQRS 中数据的整个生命周期:



数据从客户端开始,以命令格式进入后端。根据业务逻辑,它被转换为域对象并存储在数据库中。这些域对象被转换为各种读视图,并根据要求存储在不同的专用读数据库中。最后,客户端以 DTO 的形式获取这些读视图。

小结


有许多书籍和文章以各种方式介绍了 DDD 和 CQRS。在我看来,这些模式限制了我们在进行 DDD 设计时的想象力,如实体、价值对象、聚合等。这使得大多数开发人员觉得,DDD 离自己很远,很难实现,也很难实施。事实上,DDD 的概念并不复杂;相反,DDD 是为了封装业务逻辑,促进功能需求的扩展。


CQRS 就更简单了。在这篇文章中,我们从系统演进的过程出发,介绍了整个系统的设计过程和需要解决的问题,最后自然地得出 CQRS 的结论。


系统设计中没有银弹。每一次演进都是为了解决一些特定的问题。然而,它可能会带来新的问题。以本文的设计过程为例,CQRS 似乎解决了所有提到的问题,“贫血模型”和可扩展性不足,但也带来了新的问题,如数据一致性。每一种技术选择都有它的权衡,只要了解每个选项背后的所有威胁因素,就可以选出相对可以接受的方法。


即使你选择了 CQRS,在实践中,实现最终的一致性仍然有三种方法可以选择。系统设计是不断选择的结果。


这篇文章的目的是告诉你,DDD 没有那么可怕,CQRS 也没有那么复杂,只是一个决定而已。


查看英文原文:


https://medium.com/interviewnoodle/shift-from-monolith-to-cqrs-a34bab75617e

2022-07-22 15:586090

评论

发布
暂无评论
发现更多内容

vue高频面试题合集(一)附答案

helloworld1024fd

Vue

API服务网关

阿泽🧸

API网关 8月月更

React Redux 组件更新/渲染原理 connect 中的 mapStateToProps

HullQin

CSS JavaScript html 前端 8月月更

C++运算符重载(三)之递增运算符重载

CtrlX

c c++ 代码 进阶员进阶 8月月更

【LeetCode】重新格式化字符串Java题解

Albert

LeetCode 8月月更

10道不得不会的Docker面试题

JavaPub

redis Docker

Kubernetes 维护技术分享

CTO技术共享

开源 签约计划第三季 8月月更

Spring 全家桶之 Spring Data JPA(一)

小白

8月月更

OAuth Client默认配置加载

阿提说说

Spring Security OAuth

云原生(十三) | Kubernetes篇之深入Kubernetes(k8s)概念

Lansonli

云原生 k8s 8月月更

开源一夏|5分钟快速为OpenHarmony提交PR(Web)

坚果

开源 OpenHarmony 8月月更

开源一夏 | 粗暴项目监控,快速上手Spring家族的亲儿子SpringAdmin监控项目

知识浅谈

spring 开源 8月月更

前端面试 | 必知必会的10道Promise题!

千锋IT教育

canvas

Jason199

canvas 8月月更

鲲鹏编译调试及原生开发工具基础知识

乌龟哥哥

8月月更

IFIT的架构与功能

穿过生命散发芬芳

8月月更 IFIT

开源一夏 | 参与开源能让人更幸福

石云升

开源 开源社区 8月月更

Kubernetes 计算CPU 使用率

CTO技术共享

开源 签约计划第三季 8月月更

Android进阶(一)Android 发邮件与几种网络请求方式详解

No Silver Bullet

android 8月月更 邮件发送

【精通内核】计算机程序的本质、内存组成与ELF格式

小明Java问道之路

编译原理 ELF 链接 签约计划第三季 8月月更

vue高频面试题合集(二)附答案

helloworld1024fd

Vue

开源一夏 | 盘点 GitHub 那些标星超过 20 K 的 Golang 优质开源项目

宇宙之一粟

GitHub 开源 Go 语言 gopher 8月月更

开源一夏|OpenHarmony如何选择图片在Image组件上显示(eTS)

坚果

开源 OpenHarmony 8月月更

Linux配置SSH免密码登录(非root账号)

程序员欣宸

SSH 8月月更

STM32入门开发 LWIP网络协议栈移植(网卡采用DM9000)

DS小龙哥

8月月更

Kubernetes 选举机制HA

CTO技术共享

开源 签约计划第三季 8月月更

Kubernetes你不知道的事

CTO技术共享

开源 签约计划第三季 8月月更

学习Apache ShardingSphere解析器源码(一)

我不吃六安茶

ANTLR Apache ShardingSphere

软件定制开发——企业定制开发app软件的优势

开源直播系统源码

软件开发 直播系统源码 app定制开发 软件定制开发

云服务器基于 SSH 协议实现免密登录

昆吾kw

Linux SSH 云服务器

风控逻辑利器---规则引擎

转转技术团队

Java 规则引擎 风控 后端、 特征工程

从单体架构迁移到 CQRS 后,我觉得 DDD 并不可怕_架构_Chunting Wu_InfoQ精选文章