DDD和CQRS和Event Sourcing

DDD

简介

DDD是驱动领域设计,是Eric Evans于2003年提出的,离现在有17年

面向过程编程(POP),接触到需求第一步考虑把需求自顶向下分解成一个一个函数。并且在这个过程中考虑分层,模块化等具体的组织方式,从而分解软件的复杂度。当软件的复杂度不是很大,POP也能得到很好的效果。

面向对象编程(OOP),接触到需求第一步考虑把需求分解成一个一个对象,然后每个对象添加一个一个方法和属性,程序通过各种对象之间的调用以及协作,从而实现计算机软件的功能。跟很多工程方法一样,OOP的初衷就是一种处理软件复杂度的设计方法。

领域驱动设计(DDD),接触到需求第一步考虑把需求分解成一个一个问题域,然后再把每个问题域分解成一个一个对象,程序通过各种问题域之间的调用以及协作,从而实现计算机软件的功能。DDD是解决复杂中大型软件的一套行之有效方式,现已成为主流。

DDD的四层架构

分层英文描述
表现层User Interface用户界面层,或者表现层,负责想用户显示解释用户命令
应用层Application Layer定义软件要完成的任务,并且只会协调领域对象进行不同的操作。该层不包含业务领域知识
领域层Domain Layer或称为模型层,系统的核心,负责表达业务概念,业务状态信息,以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要经历放在领域对象分析上可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手
基础设施层Infrastructure Layer主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力能力时候才需要进行规划,一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现

领域对象

也叫构成要素

  • 实体(Entity),具备唯一的操作ID,能够被持久化具备业务逻辑,对应现实世界业务对象
  • 值对象(Value Object),不具有唯一ID,由对象的属性描述,一般为内存中的临时对象,可以用来传递参数或对实体进行补充描述
  • 领域服务(Domain Service),为上层建筑提供可操作的接口,负责对领域对象进行调度和封装,同时可以对外提供各种形式的服务。
  • 聚合根(Aggregate Root),,具有全局唯一ID,二十题只有在聚合内部有唯一的本地ID,值对象没有唯一ID
  • 工厂(Factories),主要用来创建聚合根,目前架构时间中一般采用IOC容器来实现工厂的功能
  • 仓储(Repository)封装了基础设施来提供查询和持久化操作

Event Sourcing

概念

提到Event Sourcing,我们会联想到一个非常相近的生活中的例子就是会计账本,会计账簿上的会计条目按照发生的时间顺序,记录了对账户余额产生变更的事件。通过会计账簿的记录,我们可以计算出任意时间点的账户余额。如果说有一类应用程序需要留存对最终结果造成改变的所有事件,那Event Sourcing就像是这类应用程序的概念抽象。所以我们看到Event Sourcing也有着和会计账簿一样的特征:

  1. 不是保留当前状态,而是保留所有导致状态改变的事件
  2. 事件会按发生顺序被记录下来
  3. 通过事件重建得到状态

听起来event Sourcing的概念也没那么难理解,更加贴合现实,还保存了所有真实的事件,如果有审计相关的需求,显然很容易得到审计需要的数据

如何使用

我了解到有的项目有基于命令转化为事件,并将事件持久化到数据库,但是在此同时他们也把command转化为snapshot保存了下来。读模型的构建全部基于snapshot。该团队确实将系统发生的真实事件全部留存了下来,但实际并没有通过事件重建得到状态,所有的状态都是来自于另外处理得到的snapshot。严格上来说并不能算做是使用了Event Sourcing,系统中做到了留存Event,但是并没有用到Sourcing。

基于一些背景信息,当时该项目使用Event Sourcing的出发点在于,客户强烈要求将DDD的思想和产出的模型完全代码化,特别是在Event Storming过程中的产出。

上面的例子不禁让我们思考一个问题:究竟在什么情况下需要用到Event Soucing?

为了回答这个问题我们先来看看Event Sourcing中的核心概念:

Event发生的事实,也是唯一真实的数据来源。用过去式来表述。时间系统中的事件是immutable(不变的),不能被更改和删除,只能通过添加新的事件来改变当前系统的状态

完全重建(Rebuild):我们可以完全丢弃系统状态,在任何时间通过事件日志按照时间顺序重演出当前系统

事件回放(Replay):就像平时浏览视频一样,如果视频总长是半个小时我们想回到25分,我们可以直接把进度条拉到25分。在event Sourcing的系统里,我们可以基于某个重建出来的系统状态,回放后续的事件,得到我们想要的某个时间节点的系统状态

又比如:我们的会计账簿里保存了过去一年全部的会计条目,现在想要得到5月30号丹田余额,再次前因为业务要求,我们已经得到了每个几度结束的账余额。那我们可以通过已知的5月31号的账户余额对5月31号发生的所有存款和取款进行反向的重放得到5月30号的余额。

好处

基于Event Sourcing的特性,我们可以来探讨下它究竟能给我们的系统或者说业务带来怎样的好处?

  1. 审计追踪:首先它留存所有真实事件的设计天然地为后期审计追踪提供了便利,因为系统里留存下了所有现实中产生的痕迹,并且这些痕迹都不被允许修改;
  2. 适应多样的查询需求:我们的系统状态都是来自于事件,那意味着我们可以根据不同的查询需求构建出不同的读模型,以适应业务需求。这也是为什么我们看到Event Sourcing会经常伴随CQRS出现的一个原因。因为在Event Sourcing的系统里我们可以利用其特性,分离读写模型;
  3. 调试:这个优点的来源同样是保存了所有的事件,这意味着当我们线上环境出问题时,我们可以把线上环境的所有event拿到一个类线上环境下测试, 找到问题出在哪儿;
  4. 可以得到系统任何时间点的状态;
  5. 系统状态可以是内存内的,不一定要持久化到数据库:任何事情发生时,就像服务崩溃的时候,我们都可以通过事件重建得到系统状态。这样你就不需要考虑持久化到数据库会涉及到的各种Data Mapping的逻辑了;
  6. 领域事件是有价值的,存下产生的领域事件,不丢失所有的现实痕迹,为支撑后期业务扩展,提供商业数据分析的数据源。

从它能带来的优点来看,当我们的业务需求有:

  1. 能够保留下所有的事件以适应审计的需求;
  2. 客户认为系统中发生的事实都是很有价值的,一定要保存下来,以便支撑后续业务扩张的商务分析;
  3. 需要经常查询不固定时间点的系统状态;
  4. 多种多样的基于不同维度的查询需求时,不妨考虑一下Event Sourcing。

当然决定用它之前我们还是得考虑一下它的缺点:

  1. 事件的版本: 对于不同类型或者不同聚合根下的事件我们有着不一样的Event Handler, 而当业务演进的过程中,相应地对事件的处理也会不同。这意味着我们在业务扩展的时候需要考虑兼容旧的事件;
  2. 业务发生改变后,为适应业务需求我们需要replay出的application state也会可能发生改变,那我们要如何兼容旧的事件rebuild或replay出新结构的application state?
  3. 让开发团队感到陌生的设计思想;
  4. 较少成熟的Event Sourcing的框架支持;
  5. 在Event store中需要序列化Event。

CQRS

简介

CQRS,即命令和查询职责分离,是一种分离数据读取与写入的体系结构模式。基本思想是把系统划分为俩个界限

  • 查询系统的状态,而且没有副作用
  • 命令,更改系统状态

CQRS(Command and Query Reponsibility Segregation)是一种与DDD不同的模式,将写与读区分开,CQRS适用于DDD的原因在于查询本身不应当影响建模

CQRS主要包含俩大概念一个是读写分离(Command&query),一个是事件源(Event source)事件源不是必须项

定义

  1. 如果一个方法改了对象状态,就是一个命令,不应该返回数据。比如创建资源的时候,不应该返回资源id
  2. 如果一个方法返回了数据,不应该直接或间接的修改对象的状态。比如有些系统在查询中实现了懒删除

根据 CQS,一个方法永远不应该同时存在。比如看栈的典型数据结构,push函数是一个命令,而top是一个查询。最后,pop 函数违反了 CQS 模式,因为它修改了堆栈的内部状态并同时返回信息。

在 CQS 模式的基础上,Greg Young 在 2010 年创造了CQRS(Command Query Responsibility Segregation)架构模式。它也将写入和读取分开,但在 API 方面。因此,它提出了单独的 API,一个专用于更改应用程序状态的命令路由,另一个专用于返回有关应用程序状态信息的查询路由。

期望解决的问题

  • 类似懒删除这种导致的数据不一致,难以排查的问题
  • 使用同一个领域对象来进行数据读写可能会遇到资源竞争的情况。所以经常要处理锁的问题,在写入数据的时候,需要加锁,读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并会影响系统的吞吐量。
  • 对于复杂的业务场景,查询通常不只是通过领域对象构成,比如商品需要从opensearch中查询。像数据层面做读写分离,缓存一样,读db和写db通常也是分离的。需要有一种结构和这种场景映射

如果进一步把CQRS的思路拿来,把API后面的数据库分成俩个数据库也是有道理的。一个应该针对写入进行优化,另一个针对读取进行优化

例如通过对一个进行大量规范化,同时对另一个非规范化。一路走来,在写入时可以确保良好的完整性和一致性

CQRS,DDD,事件溯源

CQRS经常与领域驱动设计(DDD)和Event-Sourcing结合使用,虽然这三个概念相互独立,但它们相得益彰

发送到基于CQRS的应用程序的命令API的命令也可以在DDD意义上截石位聚合的命令。然后聚合按顺序生成一个或多个事件,这些事件可以使用事件溯源存储在时间存储中,并用于聚合的稍后重放

此外,流程中生成的领域事件也被转发到事件 API,事件 API 又将它们传递给各个连接的客户端,这些客户端接收有关应用程序内技术流程的准实时更新。

此外,领域事件也被转发到应用程序的读取页面以更新那里的(预先计算的)视图。为此,使用了所谓的投影,它决定技术领域事件与哪个视图有什么相关性,然后在 CRUD 语句的帮助下相应地调整受影响的视图。

在这种情况下,事件存储代表了单一的事实来源,之后可以借助它来设置任何视图。但是,应该注意的是,写入视图与写入事件存储是分离的,因此开发人员应该熟悉 CAP 定理和最终一致性。

为什么选择CQRS

最后,问题仍然是为什么要处理 CQRS。一个明显的原因是它对 DDD、Event-Sourcing 和 GraphQL 等其他几个概念的明显补充。此外,CQRS 从一开始就将读写分离,针对分布式架构,使其非常适合用于运行在 Web 或云上的基于服务的系统。

因此,CQRS 还提供了微服务架构的所有优势,例如各个服务的可扩展性、可维护性和可测试性。使用 CQRS 也可以轻松操作不同版本的业务逻辑,并且可以具体限制各个服务的访问权限,从而有利于整个系统的安全性。

然而,缺点在于其架构本质上比传统的客户端-服务器系统更复杂——但应该记住,CQRS 也提供了几个优点,但这些都是有代价的。

CQRS 的最大优势之一是可以将技术代码与业务代码分离,特别是在与 DDD 和事件溯源相关的情况下。因此,可以在不更改技术子结构中的任何内容的情况下调整业务逻辑。同理,更重要的是反之亦然,有利于业务逻辑的长期稳定性和绝对信心。

由于提到的复杂性,建议考虑是否在已经实现 CQRS 和 Event-Sourcing 的合适框架上构建您的应用程序,以便作为开发人员,您可以主要专注于设计和编写技术代码。此类框架可用于多种技术、语言和平台,例如 Spring(Java)或 NestJS(TypeScript、NodeJS)。

框架选型

框架功能满足学习成本
阿狸colaCQRS,业务扩展点,代码自动分层官方文档较少,中文技术文章多
AxonCQRS,事件溯源,支持集群事件,saga管理复杂业务官方文档完善,中文技术文章少
activiti流程定义,流程设计器(可视化),数据持久化分离,(运行态和历史数据),支持原生spring文档完善,架构教重,深入学习成本大
spring StateMachine流程定义,时间驱动状态文档完善,轻量级,上手快
Last modification:December 12, 2022
如果觉得我的文章对你有用,请随意赞赏