事件溯源和 CQRS,我错过了啥?

Posted

技术标签:

【中文标题】事件溯源和 CQRS,我错过了啥?【英文标题】:Event Sourcing and CQRS, What did I miss?事件溯源和 CQRS,我错过了什么? 【发布时间】:2020-06-16 18:45:21 【问题描述】:

我开始阅读与 CQRS 相结合的事件溯源模式。 据我了解,CQRS 模式是一种将写入和读取操作分开的模式。 事件溯源是一种模式,其中系统中的所有内容都由触发事件的命令启动。事件溯源模式需要一个事件总线。 有几件事我没能理解。

事件存储包含发生在某个实体上的所有事件。如果我想查询这个实体的当前状态,我需要查询这个实体发生的所有事件,并重新创建它的当前状态。 所有事件历史记录都存在于事件存储中。 为什么我不能有一个微服务来负责将每个事件保存到事件数据库(如果我想记录这些事件以进行进一步的操作。比如 Kafka)和一个单独的微服务来定期更新实体上的更改数据库(例如对 MongoDB 中实体文档的简单更新)。当这些微服务完成他们的工作时,这个事件将从事件存储中删除(假设我使用队列实现了这个事件存储)。 这样,每当我需要查询实体的当前状态时,我只需查询数据库,而不是查询事件存储并重建当前状态(或根据事件存储重新计算状态并定期缓存结果) .我不明白为什么必须永久存储所有事件,为什么不是可选的?

例如,接收事件的 Lambda 函数会生成事件并将它们存储在针对每种事件类型的单独 SQS 中。每个 SQS 都有自己的 lambda 函数,负责处理相应的事件类型。事件一经处理即被移除。

【问题讨论】:

【参考方案1】:

事件溯源模式需要事件总线。

事件溯源不需要总线,除非您需要将更改(事件)通知其他系统/域。

如果我想查询这个实体的当前状态,我需要查询这个实体发生的所有事件,并重新创建它的当前状态。

嗯,有点。您只需要在处理新命令时执行此操作,并且需要验证应用该命令不会使“实体”(如您所称)不一致。请注意,这涉及到 CQRS 的 命令 端,而不是查询端。

对于查询/读取模型方面,您有很多不同的选择。使用事件溯源时,通常会使用单独的数据存储来维护事件的非规范化版本以及随着事件发生而更新的相关数据。这个单独的商店通常是Eventually Consistent,对于这个答案来说太多了。您的读取模型也可以是关系数据库、平面文件或您能想到的任何其他存储数据的方式。通过在事件发生时接收事件、通过总线、轮询数据库或其他方式,其数据与写入模型保持一致。

查询事件流并实时处理(或部分处理)它们以构造查询也是绝对有效的,但需要这样做的情况相对不常见。

所有事件历史记录都存在于事件存储中。为什么我不能 一个微服务,负责将每个事件保存到 事件数据库(如果我想记录这些事件以进行进一步操作。 像kafka之类的东西)和一个单独的微服务来更新 常规数据库中实体的更改(对实体的简单更新 以 mongodb 为例)。

你可以!

当这些微服务完成他们的工作时,这个事件将是 从事件存储中删除(假设我实现了这个事件存储 使用队列)。

您也可以这样做,但是您不是在进行事件溯源。这更像是“事件驱动架构”,它在不使用事件溯源的情况下是可能的并且完全有效,但不能提供所有相同的好处。在事件溯源系统中,事件存储是数据的真实来源,队列不是存储真实数据的有效场所,因为它并不是真的要长期存储数据。

当您进行 CQRS 时,尤其是当您进行事件溯源时,您需要改变您对“当前状态”含义的心理模型。实际的真相存储在某个地方(事件存储、关系数据库等),当您查询时,您将该真相投射到您需要的任何格式中。

例如,我有一个用户数据库,其中 FirstName 存储在一列中,LastName 存储在另一列中。代表我的行在 FirstName 列中有“Phil”,在 LastName 列中有“Sandler”。当我在 UI 中显示数据时,我将其显示为“Sandler, Phil”。为什么不将它作为“Sandler, Phil”存储在文档数据库中并完成呢?因为通过规范化数据,我已经准确记录了真相,并且可以选择在未来需要时以不同的方式投影数据。

那么上例中的当前状态是存储在两列中的数据,还是“Sandler, Phil”?在 CQRS 中,您不应该根据当前状态来考虑它,而是根据您的两个独立模型,真相(写入端)和它如何被预测(读取端)。

【讨论】:

我想我明白了。假设我有一个简单的应用程序,它接收命令,并在没有任何其他消费者的情况下生成事件。在这种情况下,我需要一个事件存储来存储当前状态(在我的应用程序中发生的事件) 如果我开始通过向事件添加消费者来复杂化我的应用程序,我需要添加事件总线,以便这些事件能够到达消费者(PubSub/ 或队列等)如果我需要对当前状态(数据)进行一些验证我将访问事件存储并投影应用程序逻辑所需的数据(在写入端)查询数据时(在读取端)我将投影数据存储在事件存储中(并且可能将结果存储在其他数据库中以提高性能)。 是的,你明白了。只是为了迂腐:你仍然没有必须有一辆公共汽车。可以只提供一个 API,让其他服务可以定期轮询新数据。也就是说,总线是(迄今为止)处理获取下游服务更新的最常见方式。【参考方案2】:

如前所述,事件溯源不需要总线,它需要事件存储。

您提到的模式(读取所有事件以重构实体状态)就是我所说的事件溯源的“领域驱动设计风格”。

您的想法与“事件+状态”导向的方法更相关。

让我们更深入地了解这两种方法。

DDD 和聚合流

DDD 战术模式之一是聚合模式。它基本上是一个一致性边界。一个命令只能应用于单个聚合实例,因此形成一个事务。处理命令时,聚合状态会发生变化,因此会产生新的域事件(或多个事件)。然后,我们将事件作为一个事务存储在事件存储中。单个实体的所有事件都存储在一个流中,我们通常称之为“聚合流”,流名称通常由聚合类型及其 id 组成(如Order-123)。

这里的目标是聚合的意义——一致性。绝对确保您在聚合的最新状态上执行命令的唯一方法是读取所有事件(或快照和快照之后的所有事件)。

我不确定您提到“查询实体状态”是什么意思。如果您的意思是“通过 id 获取实体状态” - 这似乎是正确的。对于查询,您不这样做。这就是 CQRS 发挥作用的地方。您将必要的事件投射到另一个地方,一个允许运行查询的数据库。在该数据库中,您拥有实体的预计状态。投影仅使用来自一种实体类型的事件并没有限制,它实际上更像是一种反模式。读取模型(投影状态)用于特定目的,通常由用户需求驱动(各种 UI)。

事件+状态

有很多事件源系统可以完全按照您的描述进行操作 - 将实体状态投射到另一个存储中,因此您始终拥有现成的、易于访问的实体状态,而无需重复读取事件一遍又一遍。

这听起来很吸引人,但您必须确保写入事件和更新此快照以事务方式进行。在您描述的体系结构中,当您具有将事件投影到文档数据库的功能时,它将不起作用。实体状态快照将始终是最终一致的。因此,当您执行命令时,您很容易遇到这样一种情况,它在陈旧的实体快照上运行,因此您向系统引入了一些奇怪的行为。最糟糕的是,您的所有测试都将是贪婪的,并且在系统加载时会在生产中发生。此类错误令人讨厌且难以捕捉。

关于其他事情,我相信其他答案已经涵盖了这些要点。

【讨论】:

【参考方案3】:

事件溯源(有或没有 CQRS)具体意味着存储实体的状态,通常使用特定领域的事件。当您需要运行需要来自该实体的数据的业务逻辑时,您可以将事件按顺序投射到一个状态上并使用它。

将域事件存储在 Kafka 之类的东西中是绝对有效的做法,但将实体本身(通过投影事件然后存储或其他任何东西)存储在文档或普通表单数据库中,这只是不是事件溯源。

我假设你知道事件溯源的好处,所以我不会在这里讨论它们,但请随时添加评论,我会扩展这些。

为什么不将事件存储在 Kafka 之类的东西中,并且在加载真正的事件源时不使用它们?如果您没有将快照存储在与事件相同的数据库中,那么您将面临出现并发冲突的非常现实的风险:例如重复输入、引发冲突事件,或者如果您决定最多使用 - 则会丢失事件一次引发事件的语义。这些直接意味着你不能真正依赖你发出的事件来作为真相的来源。

【讨论】:

【参考方案4】:

我不明白为什么必须永久存储所有事件,为什么不是可选的?

每Martin Fowler,(强调我的)

我们可以通过查询应用程序的状态来了解世界的当前状态,这可以回答很多问题。然而,有时我们不只是想看看我们在哪里,我们还想知道我们是如何到达那里的

这导致可以在事件日志之上构建许多设施:

完全重建:我们可以完全丢弃应用程序状态并重建它... 临时查询:我们可以随时确定应用程序的状态... 事件回放:如果过去的事件不正确,我们可以通过反转它来计算后果...

事件不能被丢弃的原因是事件本身具有价值。如果不是这种情况,那么事件溯源是一个糟糕的选择,因为它有many tradeoffs。事件溯源模式要求保存所有事件以便重复使用。

【讨论】:

以上是关于事件溯源和 CQRS,我错过了啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用事件溯源和 CQRS 的缺点是啥?

CQRS / 事件溯源 / 事件总线 / 时序

事件溯源/CQRS 读取模型 - 预测

Akka.NET 中的事件溯源和 CQRS

具有事件溯源的 CQRS 模式具有用于读/写的单个数据库

用示例程序介绍CQRS和事件溯源机制