赣州新闻网

首页 / 科技

CQRS:是什么? 为什么? 怎么做?

2020-04-07 · 赣州新闻网 · 科技

CQRS是推理特定域活动的有用模式。 但这肯定带有陡峭的学习曲线。 读取,写入,DDD,事件源,最终一致性,我们为什么要关心?

这是我在自己空间上的原始文章的交叉发布:CQRS:为什么? 以及所有需要考虑的事j L K l情。 随意看看并查看我的其他文章? 4 N { g 8 6 U。

CQRS:是什么? 为什么? 怎么做?

Photo by Franck V. on Unsplash

用例

我正在研究一个新项目,该项目旨在替换会产生后果并在整个公司中引入耦合的大型(太大)整体。

为了适应不断变化的业务需求e t t } m T ~,缩短产品上市时间,改善团队之间的可追溯性和沟通,我们决定使用CQRS异步体系结构(但不提供事件源功能,稍后再介q C P * 5 ~ U Y ?绍)。

长话短说,将所有人\"转换\"为新的思维方式并强加于人并非易事。 人们认为必须重新质疑,挑战和产生新的约束(例如最终的一致性,或者知道我们是否应该拥有某些数据)会受到质疑。} 9 $ P b 我曾经是这个家伙,总是说\"你做不到\提醒了为C 4 c 2 m - k什么。

CQRS(与DDD结合使用)迫使我们施加强大的约束,并具有我们大g p a 9 t e多数人以前没有的限制(使用经典后端+ DB)。 这并不总是一件坏事:它揭y Z ] o 5 Y 4 @示了一切。 我们对域本身了解很多,总是质疑域的分离和责任。 它使我们了解业务。

在这里,我将大致讨论CQRS及其与DDD和事件源的链接。 所d ; & J有这{ ? i u Z 3 t些A G & U 3 L E都始于一个用例,在该用例中,v 3 j J 4 ]我们需要向其他服务公开最终不一致(也就是高度一致)的数据。 CQRS不一定要最终保持` V u T - C 7 F )一致,$ x y但在我们的情况下,它是(因为异步且A Y D b 2高} t B Z 0 S 0 u q度可用)。 我看到有些人皱着眉头,因为我们应该接受最终的一致性,这是生活(和分布式系统)的一部分。 同意 但..



我们为什么需要它?

CQRS(命令查询响应隔离)是非典型的年前,但是如今,我们可以f % H找到很多有关它的内容,许多公司正在使用它并进行沟7 D 8 V 2 9 Q通。; | 1 5 我们甚至可以在不知道自己做的情q R I & Z况下进行CQRS!

模式很简单:

CQRS:是什么? 为什么? 怎么做?

CQRS并不意味着实际上可以使用微服务,消息传递基础结构或事件,2 E也不能进行DDD。 出于某种原因,它经常与这些技术一起使用。

一个写模型,多个读模型

CQRS的想法是使一个应用程序(广义上)能够与不同的模型一起工作:

它使用的一种内部模型:写模型,由Commands修改(稍后会详细介绍)

它和其他应用读取H j # D的一个} Y . 2 N ~ =或几个读取模型(我们不能让其他人读取写入模0 - n型)

读取模型可以由前端或API读取,没关系。

例如:如果我们有一个在某些资源上采用POST / PUT的API,将状态保存到P$ _ X O ^ 6 @ Z nostgreSQL,并有一个将其同步到Elasticsearch上以在我们的网页上进行智能查找的过程,那么我们几乎在做CQRS(我们可能还会公开我们的 PostgreSQL ..)。

可扩展性

CQRS允许我们独立扩展系统:我们通常必须处理更多的读取而不是写入的操作,因此具有不同的可伸缩性。

我们希望在O(1)中访问读取:我们不z c % D r e希望它们花更多的时间来获取实体,因为它具有与其他实体的更多链接:我们想避t W b免爆炸性的JOIN。 解决方案是在任何人请求之前,在数据更改时预先计算结果。 这样做还会在请求发生时使用更少的资源,从而减少延迟并使其在p999上可预测且稳定。 我们在争取空间。 稍后再详细介绍。

当我们实现Redis缓存以避免主数据/ K % W U = % V :库过载(进行写操作)时,这就是CQRS的精神。

业务需求

将写入模型与读取模型分开可以帮助我们分离域的复杂方面(谁需要什么,谁负责什么),并增加解决方案的灵活性。 我们可以更简单地适应不断变化的业务需求。

这是因为我们更多地考虑了责任:谁在变异数据? 用例是什么? 我们应该真正拥有这些数据吗? 我们会采取行动吗? 这不是其他应用程序的责任吗? 谁只需要读取数据? 应该是高度一致的吗? 由于这种思q c d $ K @维方式,CQRS通常与DDD相关联。

并发

从技术上讲,CQRS还可以通过显示并发和锁定(事务数据库)来简化它们。

当我们使用异步CQRS模式工作时,我们经常谈T Z c _论数据的最终一致性,数据生命l e D 6 5周期,数据所有权,业务需求以及许多关于建模的问8 8 = = ] @ E R 5题:实体的事务K d 9 r ^ t x U边界以及我们应始终拥有的不变式。 同样,这就是为什么CQRS通常是面向DDD的原( 4 4 0因:必须非常仔细{ 9 ] $ A g C ,地定义数据形式的集合。 (稍后会D a ! g I详细介绍)

没6 0 9 q *有\"读自己写\"的语义

必须明确处理陈旧数据。 如果我/ ^ W Q h r们更改资源(通过发送命令)并立即读取相同的资源,我们将看不到更改。 异步CQRS不提供\"读取自己的写入\"语义。

前端可以通过a } % n = 5 8 ) &进行乐观并发来模拟它:它可以嵌入一些知识,并假设它要求的G j A W / L突变会很好,因此它在获得真正的突变之前会显示它认为将是答案的东西。 如果有差异,它会适应。

当它是同步CQRS时,我们可以具有以下语@ } t ` t 5 5 X义^ { , 6 E:我们将这两个模型在同: ` T b , s g N ^一事务中写入不同的表中。 它始终是同步的。 CQRS很少是同步的,因为我们想使用不同的资源,数据库类型,使用消息传递基础结构来进行工作/扩展:我们很少能够使分布式事务跨G q + D Q ; q 5资源(我们可以谈论XA或Sagas,但是……现在不行!)。

CQRS是一种模式,严格将处理命令(变异)的职责与处理无副作用查询/读取访问的职责区分开来F 9 ) 6 + K 5 P /。

这不是万能的解决方案

我们仅应在某些情况下考虑实施C6 } m l U ~ +QRS:

我们有很多不断变化的业务需求

公司不知道确切的去向

我们有可扩展性问题

我们与其他团Q a |队合作(即:其他受x E 5 W D 0限环境)

多种服务竞争改变相同的资源

我们正在安排 c . t O L a n我们周围的其他服务

我们域中发生的事情会影响他们,反之亦然

我L Y ; # 9 -们的领$ & } E域是面向写入的,我们不会读取您自己的数据,其他应用程序会

对于单独工作的简单API或明确定义范围的CQ ! Y !RS的开销可能是巨大且不必要R U } 9的。

即使我们要实现CQRS架构,团队(开发人员和业务人员)也将需要绕过对变更的恐惧,遵循学习曲线,弯曲思路,然后适应过p : 去的工作方式。

AKF规模立方

如果我们遵循Write + Reads逻辑,则意味@ h 1 T @ Z a ^着我们将以某种方式在系统中复制数据。D p z $ 4 \"相同\"数据将出现在写入模型和读取模型下。

我遇到了一些真的很害怕重复数据h N W r !的人,或者觉得这是一种反模式。 数据只有一个主机,它应该是控制谁有权访问数据的唯一主机,没有人可以在自己的数据库或消f = ) u t Z & p息传递基础: 2 ^ g结构中复制它。 您必须始终调用我的API! 瓦德复古!

是的,有时可能需要始终调用API而不复制数据(例如GDPR下的用户数据)。 但是它很少是强制性的,它本身也可以成为反模式,因为它导致更多的复杂性,更多的依赖关系,不良的性能,服务中断,SLA减少。

我_ . c ? y 9 j喜欢AKF规模多维数据集,以了解为什么组织中的重复是横向的:d x # 1 w ~

CQRS:是什么? 为什么? 怎么做?

简而言之,三轴:

X:低级技术:我们应该复制,复制,缓存,负载均衡数据

Y:组~ { H # e织:我们应该拥有独立的服务来处理不同的领域:它们负责自己的数据(按DDD表示)

Z:高级分片:我们应该根据用例将相似的内容(按resource_id,按地理位置)细分为自己的基础结构

命令/书写:副作用

因此,导致对写模型进行写操作的事物称Q ? I + y :为命令。 这是设计我们始终使用的工具的通用术语:改变系统状态的工具(任何形式的更新)。

命令可以被同步或异步处理。

命令是否可以通过消息总线。

命令可以是简单的A. { n % IPI调用。

如果不执行OOP,则Command? . ? C ^ &可以是超类。

命令可* s A # G a W d以是简单的函数调用。

所有这些概念都与命令是正交的。 我们命令某些` U * $状态以某种方式改变。 命令是一种意图(不是事实),并且会导致副作用(对资源)。 它指向特定的目的地(不广播),并根据消费者域(共域)进行定义。

通常定义为VerbSomething:CreN n k ! 6 C @atef . ] WOrder,ShipProduct,ComputePrice,GiveMoney。 稍后我们将看到定义为OrderCreated的事件的对称性(同样,事件不在CQRS范围内,但它们运行得很好)。

命令以行为为中心,而不是以数据为中心。 它是关于更改内容的意图,它并不映射到资源的格式(例如API中的DTO)。 它可以包含有助于处理意图的数据,仅此而已。

我们还将讨论基于任务的系统,而不是基于资源的系统。 写入部分不接受新资源或现有资源的补丁:它们接受任务(也称为命令)。 它可能已经被命名为TQR# - k ] t sS :-)。

流

处理命令的流程始终相同:

如果是异步的,它将保存到消息传递基础结构中,并向呼叫者返回\"确定\"(不能做更多的事情)

在处理它时-同步还是异步-根据其形式f m B K 8 5 ` @或拒绝Command

如果被授予,它将在状态b d m : y上应用命令(可以生成或不生成事件)

➡如果未% 7 ) q ? ? a m 授予,则返回错误(同步或异步)

保存新状态

如果同步,它将返回OK到调用者-或最少的信息,例如ID,但不返回整个状态(即写模型)

➡如果异步,则将消息提交到消息传递基础结构。

CQRS:是什么? 为什么? 怎么做?

指挥部应遵循一劳永逸& ` g B Z V i的原则:除\"确定\"外,i z ?我们不应期望指挥部有任何结果。 命令只是一个要求。 处理可以是同步* ( Y T的或异步的,我们不应该尽快获得结果。 结果将稍后来自读取的部分。3 T c

命令处理程序必须包含业务逻辑,以便能够% , + ) ~ v 9立即拒绝或不拒绝命令。 例如,要处理

class OrderComman% 0 _ } T ^dHan? 2 ,dler {
...
fun cancelOrders F [ u l L(cmd: CancelOrderCommand) {
val order = repo.fetch(cmd.orderId) // the only place w9 , I R 5here we can fetch the write model
if (order.inTransit()) e& k A L p A a 8rror(\"Too late, we can\'t cancel an ordes V g @ ` : v ar in tra3 R Q _ !nsitk D c\")
// ...
val newStae _ % = C v _te = order.cancel()
reA * , o !po.persist(newState) // OR persist only events, OR both atomically (Outbox Pattern)
}

关于命令还有很多要了解的} _ i w w内容,在这里我将不做介绍,因为这不是本文的重点。

命令处理应该是幂等的

命令不能使用读取端来获取数据

仅应出于技术原因重试命令,而不得出于业务原因重播命令:结果可能会有所不同(例如:此后更改了增值税)

可以将命令Q = o [ ~保存到命令总线中以进行进一步处理

命令是关于管理系统中的副作用的。

查询/读取` t V o r ;/服务层:无副作用

如前所述,我们可以从相同的原始数据构建不同的读取模型。

当我们需要查询数据来回答不同的用例时,我们通常都会这样做。 我们在某些地方(PostgreSQL,Mongoa = Q t h Z U wDB,Cassandra,S3…)有我们的真理源,在这里我们编写/更新我们的东西,但是我们想使用专门的数据库来不同地解释它们:

缓存数据

快速文? ! ~ p 本无法)适应我们的新模式或交w j i X r流方式。 它只希望经典API重用它了解的现有格式来获取数据。 这是专用阅读服务(\"传统阅读服务\")$ U |的理想用例。 它可能需要不同的服务集来构建遗留视图,并& , (需要不同的业务规则来匹配遗留模型。

传统模型倾向于具有更多数据(由于采用整体方法),可以是跨域的(现代堆栈由} 1 V 3 * v *于DDD方法而被拆分),并且可能继承了更旧的堆栈约束。 大规模发布是F a ~ # a取数据库(v2)。5 I % x

在T3,尽管先前的更新发& ^ W /生在T2之前(绝对时间),但Reads Service发送了Order(v1)的旧版本,因为它不是最新的。 我们不知道系统收M g # j K m ( j m敛需要多长时间。 这Q U +就是为什么CQRS没有\"读自己写\"的语义的原因,也是为什么当外部系统与我们交谈时我们必须始终考虑x f p K K最终的一致性。

我们刚介绍的版本在该系统中必须具有。 这表示一个逻辑时间,一个单调的计数器,可以帮助我们了解流程的进展情况并制定决策。 它是Writes模型的一部分,并传播到整个系统(如果有事件,则读取模型…)。

版本不是我们在7 h n此类系统中可以找到的唯一人工制品。 我们还可以使用向量时钟和CRDTs结构来处理事件排序因果关系` Z W n 4。 这些通常是外部系统不想处理的事O ` & U情,这是我们内部的混乱,因此需要一个独特的读取模型来消除噪声。

有一些技术可以帮助外部系统获取他们期望的版本,稍后再进行介绍。

如果我们希望在读写数据库之间保持高度一致性,则需要对它们进行原子更B K新(由于所使用的异构系统,这种情况很少出现)。 即使这样,如果这样做,我们将失去依靠不同系统提供的可用性(CAP定理,我们可能是处于故障状态的CP或AP,而不是同时处于一致状态和可用状态)。

通过将两个系统清楚地分开,读取服务独立于o ^ H g f g写入服务工作:不同的生命周期,不同的部署,不同的约束,不同的SLA。A ` w l

一套不同的功能

读取_ C - 0服务可以提供比写入服务更多的功能。 双方的参与者和用例都不相同。S R C 这E . )状态等。他们不需要花哨的功能,流量 不重,过程很慢。 他们只是需要99.999%的正常运行时间,因为这是他们工作的一部分。

两种Reads Service都有不同的SLA(一个可以\"降级e . t t ~ N `\",而另一个不可以),功能和模型(admin ReadP K L S ; As Service将提供更多内部详细信息,对客户Reads Service而言是隐藏的)。

如何建立读服务

作为开发人员,我们喜欢测试新技术,新数据库,新系统。 拆分我们的写作方式和阅读方式的k o d 3奇妙想法为我们提供了实现这种爱所需要的东西。

创建新的Reads Service(具有其独特的Reads DatJ v 5制它以写入新系统。

而且,我们可以有多种语言。 每个阅读服务都可以用任何语言V p 0编写,以更准确地回答读者使用的用例和技术。

这是构建此类服务的技术列表:

双重写入

在代码中,当我们使用(可爱的)ORM(或不使用)写入数* _ . s p k % c *据库X时,我们添加代码或抽象以也写入Y

我们必须使用交易或适当y N m的交易来确保它们之间的原子性/一致性

数据库同步

每NK ( : +分钟复制和转换数据的一批

一种\"几乎实时\"的后台服务,每N分钟轮询一次更改

更改数据捕获(CDC)

它将数据库更改导出到事件流中v # i $ | u o x w

它依靠复制日志充当数据库发出的事件; 然后我们可以对它们进行一些流处理

这是Kafka Connect与Debezium(PostgreSQL,MySQL)的绝佳用法之一

大事记

我们订阅现有事件(在原始状态更改时发布),并根据它们建立我们的读取状态(我们可以从过去重放它们)到任何数据库

我们还可以使用Kafka Streamsx m I W及其交互式查询来创建分布式自动分片数据库。

隐藏Lambda架构

与批7 * * m e Y S处理视图合并的实时视图

最好的方法是依靠pub/sub系统的事件。 从本质上讲,它们已经使发布者与消费者脱钩。 这是CQRS通常与事E 1 K s 0件相关联的另一个原因。

DDD在哪里?

到目前为止,我们几乎没有谈论过DDD,它经常与CQRS关联(但不是强制性的)。 为什么?

我们说我们有一些命令,这些命令的处理程序g ] N ` B w E W会检查业务需求,如果可以,请更新\"状态\"并将其保存到Writes数据库中。

在DDD中,此& G F % l `状态包含在我们所谓的聚合中。 它是一棵实体树(具有实体根),该树是独立的,自治的,并且从外部和业务的角度来看始终保持一致。

CQRS:是什么? 为什么? 怎么做?

集合必须确保其实体内的交易边^ j P b D 2 p 2界:从外部看0 3 s A P 2 x p,我们永远看不到集合\"半形成\",\"半1 W i v变换\Y G I ? w M n I",\"半有效\"。 我们无法直# $ Y { } a y接访问子实体:所有内容都只能引用集合(以控制一致性)。 始终遵守业务不变性规则(例如:\"如果尚未付款,则订单不可转让\"(好的,这要取决于我们的想法了( u . c!))。

在DDDI T 9 ~ . % V中,实体具有特殊含义。 这是一个由其唯一标识而不是由其属性(例如User(userId))定义的对象I K N # S V :。 ColN f _ g k ( wor(r,g,b)` T & _ g z j H不是实体,其属性定义了它,它是一个Value Object,A O Z . $ 2 j { Q它是不可变的,因此可以共享。

聚合通常是, b ) ` } j Y ! COOP中具有所有与业务相关的代码的不可变且具有副作用的免费类(与DTO相反,DTO通常是贫乏的,即:没有业务逻辑)。 它不依赖1 } Z r $ n于任何序列化框架(没有糟糕的JSON注释,没有Avro生成的类..),它没有任何注释等。

这是企业应该理解的简单,可重复使用的代码,因为它使用的是他们的话:这是无所不在的m 1 S语言。 使用相同的语言可以减少混淆和误解,因为我们不会在\"技术\"和\"业务\"方面进行Z k ^ 0 0隐式(或宽松地)翻译。

// our aggregate with a private coZ i 8 + n d s  Rnstructor
case class Order private(id: OrderId, items: Listp 9 a E, state: OrderStatus) {
def addItem(item: LineItem): Order = ???
def removeIx ^ _ S & ]tem(item: LineItem): Ordea ] M Fr = ???
def canBeCancelled(): Boolean = ???
def startProcessing(): Order = ???
def addCoupon(): O= ? i w e 6rder = ???c o 6 t F G
def cancel(): Order = ???
// Optional: events generate0 ` ) I u 9 d x 6d by the previoC 3 ( ! F vus methods to be persist) r ~ #ed separately
private val events: List = List()
}
object Order {
// the public constructor (\"factory method\")
def apply(): Order = Order(randomOrderId(), List(), OrderStatus.CREAT{ ? ` ~ J OED)
}B ! 2 U ( 3 k 3 j


聚合由存储s O v ) M z 库(具有自己的持久化方法,已隐藏)检索并持久化。 聚合不代表基础数据库或序列化格式的确切映射。 它是面向业务的,而不是面向技术的G / ; N M 6 T Y。H J e E d f

class Order9 A BRepository {
def fetch(id: OrderId): IO[Op7 s Ction] =D 1 C 9 D ; ???
dm S ` 6 Yef savX P ! D % R Te(order, f ; x C p F: Order): IO = ???
}


回到命令,我们在处理聚合时真正发生了什么:

代码将其定向到T N u命令处理程序以对其进行处J 6 6 T理

CH从存储库中获取聚合(在命令中引用)

Command命令可以与现有资源有关,也可以请求9 a = - Q创建一个资源,例如:CreateOrderCommand

CH会检查一些业务规则,以查看命令是否符合业务规则

是的,它在聚合上调用必要的函数。

these这些函数中的每一个都返回聚合的新5 W V } Q m状态。

聚合的结束状态将保留在Writes数据库中(或c _ n d将事件存储到事件存储中)。

在复杂的域中,DDD使我们能够k T b ~ , d以商业方式构造命令和事件,每个理解该域的人都可以理解。 DDD有助于发现界限,限制责任并使$ i 6 ; ! - 2 y系统的不同部分可维护,因为它们具有\"合理性\"。 这样的系统并非仅凭开发人员的有机发展。

查找命令和聚合以及更多

查找集合及其命令(以及事件等)的一种流行做法是与开发人员,业务人= 1 ; k T员和专家进行事件风暴研讨会。

通过查找我们的域必须处理的所有可能事件,我们可以将它们重新分组并形成包含相关事件的聚T m | E / e f合。 由此,我们形成了具有凝聚力的子域(事物属于一起),我们形成了实体,集合并同意了泛在语言。

CQRS:是什么? 为什么? 怎么做?

另一种技术是域f 2 ` e故事讲述。 我们想到了一个用户场景。 我们绘制它来显示流程中的人工制品和人员(从何处,何人,往何处去,由谁,何处,谁做出反应0 1 1 5 U,谁发送东西等进行验证)。 有4个项目:演员,工? u o 4作项目(文档,交流),活动(动词),(评论)。

CQRS:是什么? 为什么? 怎么做?

如果您感到好奇,也可以检查业务模型画布。

大事记

使用CQRS广播事件不是强制性j G V i的。 这是一V v M种自然的工作方式。 更新聚合时会发出事件。 将它h 8 U 7 6 q l F们广播给\"谁想听\"(其他人和自己)。 使系统具有反应性和独立性非常有用。

这是事件列表:

case cl( s * 2 W ] P Fass OrderCreated(orde} b + n ! = MrId: OrderId, items: Li8 ] K 0 , r ,st, customer: CustomerId)
case class OrderLineAdded(orderId: OrderId, item: LineItem)
case class OrderCancelled(orderId: OrderId, reason: Option)
...


现在,我们可以看看依赖于CQRS,聚合,事件的更完整的图片:

CQRS:是什么? 为什么? 怎么做?

Raiso~ B )n dp N o T V @ o\'tre

当我们发生事件时,他们通常会成为一等公民。 这意味着一切都围绕它们构建:业务逻辑,依赖关系(\"在执行Z之前我们需要X和Y\")。

作为命令,事件是一个通用术语,它不定义其实现,而是更多h , M s 2 M W r其行为和起源。

事件是过去的事实。 它本质上是一成不变的(我们不能改变过去的权利吗?)。 与具有固定目的地的命令不同,它们可以包含其起源,是有目的的,而事件则相反:事件只是以一种\"忘却\"的方式向世界广播,是k * 2 O P , ,无目的的,并且可以是@ B 2 4 7匿名的。 因为我们不知道谁在听,: u ^ _ C b # ?所以我们无法对谁需要它进行硬编码(我们仅依靠传输事件的技术总线):这减少了系统之间的耦合。 我们可以? / U J F j p a R创建新的监听系统,而无需发出系统来了解和关心。

CQRS:是什么? 为什么? 怎么做?

这就是像Kafka这样的发布/订阅系统c ~ | 4 R 5 v w U有用的地方,因为它们是这种中间总线,可以保持事件,处理可以动态添加或删除的分发给消费者的东西等等。

重要的是,事件在生产者域中定义。 在使用者域中定义了一个命令(或N c - ` f更通用的\"消息\")(我们说目标的语言,给它一个\"# . Z y h ^命令\")。

投影事件

事件可以由任何事物创建。G u 在9 K | ( Y D A /执行CQRS和DDD时,它们主要由聚合创建。 更改汇总后,它将发出一个或多个与更& 0 V ] & M ,改相对应的事件。 事件还可以由诸如调度程序之类的外部事物产生,这取决于时间:\"在小时结束时发送此事件X\"。

在这4 . B f B 7 p ` J里,我们x Z ]可以选择是否进行事件采购。

我们可以决定独立于聚合事件的生成来更新其状态:

def increv r Dment(state: State): (State, Event)F 3 o ? =7 N N U & t t K {
val newState = state + 1
(newState, NumberWasIncremented(1))
}
val (newState, event) = increment(5)


我们可以决定通过\w . x e / h"播放\"当前状态的事件来创建聚合的新状态:

def increment(): Event = NumberWasIncremented(1)
def apply(state: State, e: Event): State = e match {
case NumberWh z 4 $asIncremented(x) => state + x
}
val newStV 6 date = apply(5, increment())

Event-Sourcing方式可能看起来过度设计。 我们还需要一个功能应用和模式匹配。 我们已经将事L e g 8 o Q件创建的逻辑与事件应用程序分开了。 这样做比较容易。 申请活动时,我们不| D M必知道活动的来源。

一个事件是轻量级的,它仅与变化有关。 通过重播其生命中的所y l _ q - J I 8 g有事件,我们可以从头开始创建聚合。 我们对发生的事情有更多的了解,我们不仅保留最新状态,也不保留更改之间的快照。 我们自己保留更改。 状态是一个汇总,是事件的投影。 我们经常具有一次重播所有内容的功能,有时称为计算器,reduce,replayer:

def applyAll(initialStateW } N w ] * y: StatA L k k ` A qe, events: List): State = {
events.foldLeft(initialState) { case (s, e) => apply(s, e) }
}


进行事件采购的优势之一就是我们还可以以不同的方式重放事件:这将q y D形成不同的状态。 当我们的模型经常变化时,! J i 4 + 4中发生了某些事情! 过去不会改变,因为我们想添加新功能。 我们只能解释过去。

我们可能有现有的用: ( q ^ { : N例来处理} l X { w T事件,但是将来我们可能会发现新的用例。 这就是为什么我们不想L + } i z丢弃任何东西,而我们希望存储所有事件的原因:以后以未知的新方式(出于可追溯性和更多原因)处理它们。 事件是黄金。 不要失去他们。

事件并不意味着事件源

不是因为我们要处理事件B I a l n v,而是必须进行事件源。 如果我们不从事件中构建汇总:我们不是从事件中进行采购,因此不是事件中的采购。 QED。

(或没有)事件来源的逻辑隐藏在AggregateRed ? apository的实现中。 这只是获取集合的一种特定方式。

当我们? G - f n进行事件采购时,我们不是直接从数据库中获取b ; X S最新状态,而是要求事件存储获取有关特定aggregationId的所有事9 Y t x @ F件(因此,对于事件存储,Kafka不是一个好的选择,但是Ka} Y N Ufka Streams可能是 B / j , A Q ,以及\"互动式查询\"),我们会重播它们以建立最新状态。 这也意味着我们必须将所有事件保留在历史记录中以进S J # 0行事件搜索。

有一{ h ` P V n W X d些策略可& y [ g ~以每X个事件对状态进行快照,从而避免在大量更V h U R新聚合时每次重播10000个事件。 但这是另一篇文章。

事件源是CQRS的正交概念。 我们可以一无所有。

智能端点和哑管

马丁福勒(Martin Fowler)介绍了\"智能端点,哑管\E X O T | D % ("的概念。 简而言之:不要在传输机制中加入业务逻辑。 我们必须控制我们的业务逻辑。 我们不希望某些管道成为瓶颈(+ L } $ j a A因A e X q . p 1为ESB成为大公司)。 运输机制应该保持愚蠢:它N x v 7 t x在技术方面E 7 ,就在那里6 t q,仅此而已。

这让我想起了ReactJS,我们在这里谈论:

-愚蠢的组件:纯净,呈现一段UI,无逻辑(按钮,| C 9 k 7 v /日% Y l历,菜单)-智能组件:处理数据,具有状态和一些业M H . 7 / n G I务逻辑。

智能组件依赖于哑组件。 我们可以更改智能组件的b ( ) = h y实现方式,只需更改其使用的哑巴组件o O C v U ? 4 D即可:我们不会更改其逻辑。

发布或使用的系统很聪明:它们处理映射,域转换等(使用反腐败层,当不同的有界U R / X # @ ( Y上下文相互交谈时使用DDD概念)。 它具有更高的可扩展性,h i 通常更易于维护,因为每个人都有自己的逻辑并提出要求。 自己动手

让某些州居住在ESB中也很常见,因为我们想根据过去或某些动态情况来更改行为。 智能终端将P k ( e ( T k W创建并维护自己的, e g c 5 J @ Y私有状态。 这不是\"管道\"的作用。

格式和语义

我们知道为什么发出事件很好,但是应该遵循什么格式? 有标准吗? 我们应该存入他们什么? 那元数据G ( p l 5 `呢? 如果我不进行事件采购但想生成事件该怎么办?有d [ k F什么区别? 事件是* O W一个简单的词,但具有巨大的语义。

没有什么是\"官方的\",但是我们可以依靠或从中获得启发:

这提供了与JSON-LD兼容的基于JSON的语法。

语境

一种活动

演员

一个东西

潜在目标

CloudEvents更有用,更完整,它提供了不同语言,不同序列化格式(Json,Avro,Protobuf等)的规范和SDK,并且可能最终落入CNCF(云原生计算基金会)。 它是面向无服务器的,但是事件是事件吧?

{
// CloudEvents mel = @ w Itadata` b f . ? P : data goes here
\"eventType\": \"order.line.added\",
7 - R 9 F s A 2 1\"eventID\": \"C1c r Y ^234-1234-1234\",
\"eventTime\":A h % G W \"2019-08-08T14:48:09.79 , d .69Z\",
\"eventTypeVersion\": \"1.0\",
\"source\": \"/orders\",
\"extensions\": {},
\"coI e )ntentType\": \"application/json\",= } o 3 R 1 $ E D , k多,对未来的发展越好。 我们将能够重构模型而无需更改事件。

与外部模型一起使用的外部事件是供公众使用的。 它们经常被非规范化(包含名称,地址,没有版本,更简单),以避免消费者理解依赖关系。

将内部事件暴露给外部会导致复杂的v E N $ * U x体系结构,困难的开发,缺乏可见性并模糊域边界。 如果我们这样做,却没有注意到,就将内部事件转换为外部事件。 因此,- E 0 q i a } ?这{ B 3 ] :也将我们的内部模型转换为外部模型。 我们正在与其他服务之间引入强大的耦合。

这就是c I @ V J F o T ; b V 2 ]tem Transactions,OrderShipped等)来构建状态,他们需要 订单,但OrderShipped只能具有orderId)

他们想查询一些读取服务以获取缺少的内容

➡这会引入最终一致性:他_ V C M k , f们可能无法接受。

CQRS:是什么? 为什么? 怎么做?

因此,一种解决方案是将状态嵌入K v k .事件本身。 这是事件进行的状态转移。

想象3个简单事件:

{ type: \"OrderCreated\", orderId: 1337, customer: \"john\" }
{ type: \"OrderLL ~ 6ineAdded\", orderId: 1337, item: { ref: \. % G M"prodA\", qty: 2 } }
{ type: \"OrderShipped\", orderId: 1337 }


如果我们的电子邮件服务仅侦听OrderShipped事件,则其中没有商品,因此需要查询可能尚未处理OrderLineAdded(滞后,网络问题)的读取服务。 发送Q Y c + = d , ` ]的电子邮件中不会包含任何内容,或更糟的是,阅读服务甚至可能不知道orderId 1337。

一种解决方案是在事件上添加一个版本(每个事件都会增加其表示的聚合的版本),然后我f % / T们可以在Reads Service中请求特定版本:

{ type: \"OrderCreated\", orderId: 1337, v: 1, custom9 }  l /"OrderCreated\", orderId: 1337, custx $ F S romer: \"john\",
state: { id: 1337, customer: \"john\, 0 2 2 q @", items: , state: \"CREATED\" }
}
{ type: \"OrderLineAdded\", order5 M HId: 1337, item: { ref: \"prodA\", qty: 2 },
state: { id: 1337, customer: \"john\", items: { ref: \"p9 x a C # ArodA\", qty: 2 } }, state: \"CREATED\" }
{ type: \"OrderShipped\", orderId: 1337,
state: { id: 1337, customer: \"john\", items: { ref: \"prodA\", qty: 2 }, state: \"SHIPPED\" } }
}


CQRS:是什么? 为什么? 怎么做?

对于消费者而言,这是@ 7 # O一项毫无H D / & 4 5头脑的工作。 他们只需要阅读\"状态\"字段,该字段就始终使用相同的模型(这里是1 O k ( = 8 p * j\"订单写入模型\")来完成工作。

开销现在在消息传递基础结构上 x m s X ; ] w义\"。 它将起作用,但是由于本文列举的所有原因以及CQRS体系结构中的反模式,这将是一个严重的错误。 我们决定使用CQRS,我们必须尊重它的工作方式。

除了写模型的m 4 A H c C系统之外,没人应该知8 * B 道写模型。

由于可伸缩性原因,我们选择CQRS

如果我们向读者展示我们的Write Model,那么它就不是任何人的Write Model,而是Write / Read模型h @ c ~ O x e U i,没有区别了,我们无法以不同的方式缩放它们。

我们无法公开发布包含写入模型(事件承载状态)的事件

如N r { ) t d 7果另一个服务必须绝对拥有我们的最新书面数据,我们应该问自己:

为什么,它的业务案例是什么?

为什v k P ? 1 = Y 7 n么它不E 6 q r A } 1 # S能处理最终一致性? 有事实和数字支持的真正原因吗?

为什么我们的事件未X { A r k d W ) V涵盖其用例?

我H C p 0们8 M { S P 8 e应该发送命令而不是被动地发送命令吗?

我们是否有错误的域分隔? (应该在使用Commands而不是在外面)

ACL抢救

解决我们的Write模型的私密性(而不是解决可伸缩性问题)的最后一种t # c N解决方案是在Writes Service中编写一个ACL(反腐败层)。 我们可以将写入模型转换为读取模型。

即时运行,但将我们的Writes Service与读取结合起来:

CQRS:是什么? 为什么? 怎么做?

或将其保存在数据库中,以避免即时 $ Q g % 4 b i转换(仍然耦合):

CQRS:是什么? 为什么? 怎么做?

在后一种情况下,必须在同一事务中以原子方式保存写模型(或事件存储中的事件)和此读模型(在发生故障时不会不一致)。 另外,z o p O } j我们也无法调用外( = u部API:如果它们失败了,我们将无法完成转换,因此不会提交事务,因此所有命令都将失败。 SLA =最小值(取决于SLA)。 这将破坏CQRS的目的。




结论

对3 0 j ] )企业

当我们习惯使用CQRS时,它是推理整个系统的好模式。J X P 肯定存在陡峭的学习曲线,以了解分离和约束。 而不是全局考虑,我们必须本地考虑(命令,事件)。 很难预测总体行为。 这些功能分为几部分,我们可以o 3 [ N c 8 3 0有不同的参与者和不同的时间表来完成它们(更多情况下是失t j I i Z K y _败的)。 最终的一致性还可以增加我们以前采用整体式设计所没有的问题。 我们对可扩展性和灵活性(CQRS)进行控制贸易。

通常,CQRS会事先结合良好的DDD思想,以避免混淆不同的域(或我们不应该使用的反向拆分域),或者创建毕竟不适合业务的奇怪聚合。 写和读模型之间的区别有很大I l ? H =帮助。

我经常问:\"为R q i w什么在写入模型中有此属性,我们对此有业务规则吗?* Q 0 N w X A\"。 如果答案是\"否,但这是针对下游内部服务的。\"那么我知道它不属于这里。 WriS V Q u Z C $tes a 0 z 4 Model上的每个属性都应在至少一个CommandHandg P h o 6 +ler中具有一条业务规则。

关于Read模型,完全是YOLO,FFA,因为这无关紧要s 6 g 7 ?,没有影响。 它的目的是根据前端或消费者的需求进+ ; z K % 6 B * o行大量改进。 我只想保护写模型。

CQRS还可以与事件一起使V F t $ T用。 他们是关于生意的。 它们提供了可追溯性,并在其他方面解耦了系统。 Event Sourcing方法本质上非常有用且非常安全,但是它附带了自己的约束(性能,快照,存储..)。 处理事件意味着我们拥有一个事件驱动系统。 这样,一项功能可能需要通过消息总线进行20跳才能完成(一系列边界事件处理程序/命令处理程序)。 如果没有良好的性能调整和o ~ 3 T m q `良好的系统可观察性(日志/指标/跟踪,以及对事物之间如q K w n e p 9 ( l何关联的良好理解),它的性能可能会很差。 即使可以扩展,也不会有效。

..高级内容!

此外,必须注意一些技术上的Y P H l O K = } ;预防措施,例如幂等和一H 7次处理(由于瞬态网络问题,我们不想对一个事件进行两次V 1 i f处理)。Q , y v & 给定聚合的事件必须按顺序处理(您不希望在OrderCreated之前添加OrderItemAdded)。 事件还意味着我们将讨论事件流处理。

此外,当在处理事件的中间发生错误(反序列化问题,意外值,网络错误)时,我们的处理可能会停留在处理大型功能的过程中(拆分为多个事件):我们该怎么办? 结果是什么? 我们如何发出错误,谁来消耗它? 我们如何联系用户以告诉他(命令已经消失了一段时间)? 我们如何确保状态一致? 我们甚至没有h e S N }谈论Sagas和错误管理,因为这是一个完c + 3 I j ; O 2整的故事。

我们还必须考虑对聚Y Q 4合和事件进行版本控制,必要时管理因果关系(向量时钟或矩阵时 t u X B D钟)。

所有这些问题不是由CQRS本身引起的,而是因为我们正在使用分布式系统。 如果4 S S您的公司希望从整体架构过渡到CQRS架构,则需要考虑所有方面:这是您需要坚强的肩膀(..要哭泣)的地方。

这是我在自己空间上的原始文章的交叉发布:CQRS:为什么? 以及所有需要考虑的事情。 随意看看并查看我的其他文章。

谢谢阅读!


(本文翻译自Stphane Derosiaux的文章《CQRS: What? Why? How?》,参考:https://medium.com/@sderosiaux/cqP v ! I _ L I _ Nrs-what-why-how-9455. - = w s y434823( $ I13)

库存贸易测试确保项目企业发展石城是什么