事件溯源和命令查询职责分离简介

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

事件溯源 (es) 命令查询责任分离 (cqrs) 的概念已经存在了很长一段时间。它们在 Java 社区中受到越来越多的关注,尽管它们似乎在 .net 方面更受欢迎。

在过去的几个月里,我在 Oasis Digital 实施了 一个具有这种架构的系统 。在努力的过程中,我们的目标是灵活性和做出正确权衡并选择我们自己的工具的能力。在此过程中出现了一些有趣的解决方案,这些解决方案对于 axon 等现有框架可能是不可能的。

我将在未来写更多关于其中一些作品的文章。在我们到达那里之前,让我们首先简要介绍事件溯源和命令查询责任分离。

CQS

命令查询分离(cqs)的主要思想是所有操作都是:

  • 命令,改变系统状态,
  • 查询,从系统中获取一些信息。

一个或另一个,永远不会两者兼而有之。例如,如果一个命令改变了系统中的任何东西,它不应该被用来读取它的状态。无突变读取访问应该始终是可能的。要求系统 更改 某些内容以读取值似乎是完全错误的,无意中更改状态的 查询 非常令人困惑,令人惊讶并且充其量会导致错误。

这些原则可适用于所有层面。 “干净的代码”中的鲍勃叔叔 称其为良好功能设计的主要原则之一。该模式也适用于系统设计。

系统架构中的cqrs

命令查询责任分离 (cqrs) 是一种受 cqs 启发的系统架构模式。它将系统分为两个不同的部分,将用于编写(执行命令)的组件与用于查询的组件分开。在这样的系统中,我们可以找到对应于这两种模型的两种请求。

ac代表自主组件


首先是 命令 ——命令系统做某事或改变其状态。一段业务逻辑更新 域模型 (或拒绝命令)并让客户端知道更改已被接受(或未接受)。

业务逻辑通常使用 领域驱动设计 来实现,但它也可以是事务脚本或任何其他适用的技术。实际上,我们最终在系统的一个区域中混合使用了这两者。

每当有任何变化时,领域模型都会以某种方式通知所有人——例如通过发布领域事件。这些事件由 视图模型接收,视图模型 更新它们自己的状态表示,与域模型分开。

这将我们引向第二种请求: 查询 ,从系统获取信息而不改变其状态。这些只使用视图模型,而不是域。

这种模式特别强大的是关注点分离。

域(写)端是关于业务的。它不关心查询、向用户显示此信息的所有不同方式、性能、用于这些目的的最佳存储等等。

另一方面,查询(读取)端是关于读取访问的。它的主要目的是使查询变得快速和方便。

虽然域只实现一次,但同一数据可以有多个视图模型。他们经常使用不同的数据库。他们甚至可以使用不同的技术——各种 nosql、规范化和非规范化 sql、内存表示、lucene 索引、olap 多维数据集等。

阅读模型也可以自由使用不同的语言,如果它们使任何事情变得更容易的话。在 scala 中实现域模型没有障碍,但在 clojure 甚至 sql 存储过程中实现视图模型。

总的来说,视图模型是非常好的(而且安全的)创新候选者。

与域模型不同,视图模型中的代码质量不必是完美的。它们都是关于重塑数据并四处移动以方便读取。一些捷径和肮脏的技巧可能是可以接受的,只要它们不会使整个事情变得无法维护。

阅读模型通常是非规范化的,准备以最佳方式回答具体问题。它甚至可以像对系统将处理的每个查询预先计算的答案一样极端,以一些简单的键值方式存储。

我们经常将视图模型称为“投影”——因为它们将领域事件“投影”到特定模型,只保留必要的信息,并以最佳形式为它们所服务的查询保留信息。

请注意,所有领域逻辑仅在领域(写入)模型中实现。它在正确的地方完成了一次。即使一个值是直接导出(计算)的,只要它具有业务意义或使用某些业务逻辑计算,它就属于领域模型。

事件溯源

另一种常用于 cqrs 的模式是 事件溯源

简而言之,这意味着系统中的所有数据都以事件的形式存储在事件日志中。事件是一条信息,表明某事已经发生(创建用户、更改名称、添加送货地址、提交订单、交付订单)。他们总是用过去时,说 发生 了什么事。

事件永远不会改变。您永远无法删除或更新它们。如果发生了什么事,那就已经发生了。如果这是一个错误,可以通过生成新的“反向”事件的补充动作来纠正,但是没有回头说它没有发生。

结合es和cqrs

事件溯源和命令查询责任分离非常适合彼此。这是一种强大的协同效应:由于组合,它们中的每一个都变得更加强大。

当命令进入时,域模型计算系统的新状态并可能发出一些新事件(这是持久化更改的唯一方式)。当同一逻辑区域收到另一个命令时,域模型从过去的事件中恢复,并通过生成一些新事件来响应新命令。这些事件代表具有商业意义的具体变化。从技术上讲,它们是系统状态的“增量”(或“差异”)。

视图模型简单地处理这些事件,只选择它们感兴趣的事件,并更新它们的状态以支持未来的查询。

好处

使用这种方法有很多好处,让我只指出其中的几个。

cqrs 自然会导致 基于任务的 uis 。每个动作都代表一些非常具体的业务事件。更改某人的名字、更改他们的送货地址或使他们成为金牌客户之间可能存在巨大差异。如果最终用户想要更改名称,他们会得到一个小表格,其中包含该操作所需的内容。如果他们让客户成为金子——那就是另一种形式的另一种行为。

将其与传统的 crud、类似电子表格的系统进行对比。他们没有“更改名称”或“将客户状态更改为黄金”之类的操作。用户所能做的就是“改变用户”。当一个特定的领域突然发生变化时,实现改变某些东西的逻辑变得更加困难。验证是一场噩梦。如果不在顶部增加更多的复杂性,审计和简单地查看什么时候发生了什么以及用户做了什么是不可能的。

对用户来说也更难——用例必须在他们的脑海中实现,知道什么时候改变什么才能达到预期的效果。

与上述相关,以及为什么 cqrs 经常与领域驱动设计一起使用的原因是系统的形状 自然地映射到业务上下文和用例 。命令对应于具体的用户意图,查询旨在回答具体问题。再一次,它与美化的类似电子表格的数据库前端完全相反。

它也适用于更高级别:业务的不同领域(ddd 术语中的限界上下文)可以作为单独的模型来实现。例如,仓储上下文可以用与销售上下文完全不同的方式表示一本书。人们可能对其尺寸、重量和库存数量感兴趣。另一个——作者、流派、封面图片、出版商描述等。

事件溯源也 使报告更容易 。根据定义,它永远不会丢失信息。也许昨天您不需要知道用户在购物车中添加和删除商品的频率,而只关心他们最终提交的订单。但是今天,企业想要追踪这些信息,这样也许他们可以发现客户想要但改变主意的物品。在未来的广告中用这些项目来吸引他们可能是值得的。

在“传统”系统中应对这样的变化将是一项不小的努力。使用 es+cqrs 很可能只需要对已经存在的数据进行另一个简单的投影,并立即 回答有关过去的问题!

最后,另一个明显的好处是 性能 。由于视图模型是分开的,它们可以有非常不同的模式。避免连接,保持数据非规范化,在线性甚至常数时间内回答许多问题。只读访问更容易扩展,写入端也不再关心查询。

费用

es+cqrs 并非没有成本,也不是所有系统的最佳方法。更多关于这个在未来的帖子。

更多资源

就像我一开始说的,这些想法已经存在好几年了。在线、书籍和会议上提供了大量资源。这篇文章只是触及了表面,只是作为(又一次)对该主题的简单介绍。

这里有一些大师的链接:

这篇文章也出现在 绿洲数字博客 上。