dim*_*vin 5 design-patterns cqrs event-sourcing
我开始阅读结合 CQRS 的 Event-Sourcing 模式。据我了解,CQRS 模式是一种我们将写入和读取操作分开的模式。事件溯源是一种模式,其中系统中的一切都由触发事件的命令启动。事件溯源模式需要一个事件总线。有几件事我没能理解。
事件存储包含发生在某个实体上的所有事件。如果我想查询这个实体的当前状态,我需要查询这个实体发生的所有事件,并重新创建它的当前状态。
所有事件历史记录都存在于事件存储中。
为什么我不能有一个负责将每个事件保存到事件数据库的微服务(如果我想记录这些事件以进行进一步的操作。比如 Kafka)和一个单独的微服务,它可以定期更新实体上的更改数据库(例如对 MongoDB 中实体文档的简单更新)。当这些微服务完成它们的工作时,这个事件将从事件存储中删除(假设我使用队列实现了这个事件存储)。这样,每当我需要查询实体的当前状态时,我只需查询数据库,而不是查询事件存储并重建当前状态(或根据事件存储重新计算状态并定期缓存结果) . 我不明白为什么必须永久存储所有事件,为什么它不是可选的?
例如,接收事件的 Lambda 函数会生成事件并将它们存储在针对每种事件类型的单独 SQS 中。每个 SQS 都有自己的 lambda 函数,负责处理相应的事件类型。处理完事件后将删除该事件。
事件溯源模式需要一个事件总线。
事件溯源不需要总线,除非您需要将更改(事件)通知其他系统/域。
如果我想查询这个实体的当前状态,我需要查询这个实体发生的所有事件,并重新创建它的当前状态。
嗯,有点。您只需要在处理新命令时执行此操作,并且需要验证应用该命令不会使“实体”(如您所称)不一致。请注意,这涉及CQRS的命令端,而不是查询端。
对于查询/读取模型方面,您有很多不同的选择。使用事件溯源时,通常有一个单独的数据存储来维护事件的非规范化版本以及在事件发生时更新的相关数据。这个单独的商店通常是最终一致的,为了这个答案的目的,这太多了。您的读取模型也可以是关系数据库、平面文件或您能想到的任何其他存储数据的方式。通过在事件发生时通过总线、轮询数据库或其他方式接收事件,它的数据与写入模型保持一致。
查询事件流并实时处理(或部分处理)它们以构建查询也是绝对有效的,但这种情况相对不常见。
所有事件历史记录都存在于事件存储中。为什么我不能有一个负责将每个事件保存到事件数据库的微服务(如果我想记录这些事件以进行进一步的操作。像 kafka 之类的东西)和一个单独的微服务,它可以定期更新实体上的更改数据库(例如在 mongodb 中对实体文档的简单更新)。
你可以!
当这些微服务完成他们的工作时,这个事件将从事件存储中删除(假设我使用队列实现了这个事件存储)。
你也可以这样做,但是你没有在做事件溯源。这更像是“事件驱动架构”,它在不使用事件溯源的情况下是可能且完全有效的,但并没有提供所有相同的好处。在事件源系统中,事件存储是数据的真实来源,而队列不是存储真实数据的有效位置,因为它并不真正意味着长期存储数据。
当你做 CQRS 时,尤其是当你做事件溯源时,你需要改变你对“当前状态”意味着什么的心理模型。实际真相存储在某处(事件存储、关系数据库等),当您查询时,您将该真相投影为您需要的任何格式。
例如,我有一个用户数据库,其中一列存储 FirstName,另一列存储 LastName。代表我的行在 FirstName 列中包含“Phil”,在 LastName 列中包含“Sandler”。当我在 UI 中显示数据时,我将其显示为“Sandler, Phil”。为什么不将它作为“Sandler, Phil”存储在文档数据库中并完成它呢?因为通过规范化数据,我已经准确地记录了真相,并且可以选择在未来需要时以不同的方式投影数据。
那么上面示例中的当前状态是存储在两列中的数据,还是“Sandler, Phil”?在 CQRS 中,您不应该根据当前状态来考虑它,而是根据您的两个独立模型、真相(写入端)以及它如何被投影(读取端)来考虑它。
如前所述,事件溯源不需要总线,它需要一个事件存储。
您提到的模式(读取所有事件以重构实体状态)就是我所说的事件溯源的“领域驱动设计风格”。
您的想法与“事件+状态”导向的方法更相关。
让我们仔细看看这两种方法。
DDD 和聚合流
DDD 战术模式之一是聚合模式。它基本上是一个一致性边界。命令只能应用于单个聚合实例,因此形成一个事务。处理命令时,聚合状态会发生变化,因此会产生新的域事件(或多个事件)。然后我们将事件作为一个事务存储在事件存储中。单个实体的所有事件都存储在一个流中,我们通常将其称为“聚合流”,流名称通常由聚合类型及其 id(如Order-123)组成。
这里的目标是聚合的意义——一致性。绝对确保在聚合的最新状态上执行命令的唯一方法是读取所有事件(或快照和快照之后的所有事件)。
我不确定您提到“查询实体状态”时的意思。如果您的意思是“通过 id 获取实体状态” - 这似乎是正确的。对于查询,您不要这样做。这就是 CQRS 发挥作用的地方。您将必要的事件投影到另一个地方,一个允许运行查询的数据库。在该数据库中,您有实体的投影状态。投影仅使用来自一种实体类型的事件没有限制,它实际上更像是一种反模式。读取模型(投影状态)用于特定目的,通常由用户(各种 UI)需求驱动。
事件+状态
有很多事件源系统可以完全按照您的描述执行 - 将实体状态投影到另一个存储,因此您始终拥有现成的,易于访问的实体状态,而无需一遍又一遍地读取事件.
这听起来很有吸引力,但您必须确保写入事件和更新此快照以事务方式发生。在您描述的体系结构中,当您有一个将事件投影到文档数据库的功能时,它将无法工作。实体状态快照将始终最终一致。因此,当您执行命令时,您很容易遇到这种情况,它在陈旧的实体快照上运行,因此您向系统引入了一些奇怪的行为。最糟糕的是,您的所有测试都将是贪婪的,并且会在系统加载时在生产中发生。这样的错误是令人讨厌的并且难以捕捉。
关于其他事情,我相信其他答案已经涵盖了这些要点。
事件溯源(带或不带 CQRS)具体意味着存储实体的状态,通常使用特定于域的事件。当您需要运行需要来自该实体的数据的业务逻辑时,您可以将事件按顺序投影到状态上并使用它。
将领域事件存储在 Kafka 之类的东西中是绝对有效的做法,但将实体本身(通过将事件投影到然后存储该事件或其他任何内容)存储在文档或普通形式数据库中,这不是事件源。
我假设您知道事件溯源的好处,所以我不会在这里详细介绍它们,但请随意添加评论,我将对此进行扩展。
为什么不将事件存储在像 Kafka 这样的东西中,并且在加载真正的事件源期间不使用它们?如果您没有将快照存储在与事件相同的数据库中,那么您将面临出现并发冲突的非常现实的风险:例如,双重输入、引发冲突事件,或者如果您决定使用至多-,则会丢失事件引发事件的一次语义。这些直接意味着您不能真正依赖您发出的事件作为真相的来源。
| 归档时间: |
|
| 查看次数: |
3028 次 |
| 最近记录: |