事件溯源 - 为什么需要专用的事件存储?

5
我正在尝试首次实现事件溯源/CQRS/DDD,主要是为了学习目的。其中有一个事件存储和消息队列(例如Apache Kafka)的概念,事件从事件存储流向Kafka Connect JDBC/Debezium CDC => Kafka。 我想知道为什么需要单独的事件存储,因为听起来Kafka本身具有其主要功能和日志压缩或配置日志保留以进行永久存储。我应该将我的事件存储在专用存储(如RDBMS)中以供Kafka使用,还是直接将它们馈送到Kafka?

enter image description here

4个回答

4
许多关于的文献来自于[领域驱动设计]社区。在早期形式中,CQRS被称为DDDD... 分布式领域驱动设计。

领域驱动设计中的一个常见模式是拥有一个领域模型,以确保您持久存储中的数据完整性,也就是说,确保没有内部矛盾...

我想知道为什么需要单独的事件存储,因为它似乎可以通过Kafka本身的主要功能和日志压缩或配置日志保留实现其目的。

因此,如果我们想要一个没有内部矛盾的事件流,该怎么做呢?一种方法是确保只有一个进程有权限修改流。不幸的是,这会留下一个单点故障--进程死亡,一切都结束了。

另一方面,如果您有多个进程更新同一流,则存在并发写入、数据竞争和矛盾的风险,因为一个编写者可能还没有看到另一个编写者所做的内容。通过使用事务或比较和交换语义,我们可以使用RDBMS或事件存储来解决此问题;如果有并发修改,则拒绝尝试使用新事件扩展流。此外,由于其DDD遗产,持久存储通常被划分为许多非常细粒度的分区(也称为“聚合”)。一个购物车可能合理地有四个专门的流。
如果Kafka缺乏这些功能,那么它将是事件存储的一个糟糕替代品。KAFKA-2260现在已经开放了四年多,所以我们似乎缺乏第一点。从我从Kafka文献中能够理解到的,它对细粒度流也不太满意(虽然我已经很久没有检查过,也许情况已经改变)。参见: Jesper Hammarbäck在18个月前写的关于这个问题的文章,得出了与此处表达的类似的结论。

4
Kafka可以用作DDD事件存储,但是由于缺少某些功能,如果这样做会出现一些复杂情况。
人们在聚合事件源方面使用的两个关键特性是:
1.通过仅读取该聚合的事件来加载聚合 2.当并发地为聚合编写新事件时,确保只有一个编写器成功,以避免破坏聚合并打破其不变式。
目前Kafka都做不到以上两点,因为1通常需要每个聚合类型一个流(每个聚合一个流不可扩展,也不一定是理想的),所以没有办法仅加载一个聚合的事件,而2则无法实现,因为尚未实现https://issues.apache.org/jira/browse/KAFKA-2260
因此,您必须以这种方式编写系统,以使不需要功能1和2。可以按以下方式完成:
  1. 不要直接调用命令处理程序,将它们写入流中。每个聚合类型都有一个命令流,由聚合ID分片(这些不需要永久保留)。这样可以确保您每次只处理特定聚合的单个命令。
  2. 为所有聚合类型编写快照代码
  3. 处理命令消息时,请执行以下操作:
    1. 加载聚合快照
    2. 针对其验证命令
    3. 编写新事件(或返回失败)
    4. 将事件应用于聚合
    5. 保存新的聚合快照,包括事件流的当前流偏移量
    6. 向客户端返回成功(通过回复消息可能)

唯一的另一个问题是处理故障(例如快照失败)。这可以在特定命令处理分区启动期间处理 - 它只需重播自上次快照成功以来的任何事件,并在恢复命令处理之前更新相应的快照。

Kafka Streams似乎具备使这个过程变得非常简单的功能-您可以将命令的KStream转换为KTable(包含聚合ID键入的快照),并拥有包含事件的KStream(和可能包含响应的另一个流)。 Kafka允许所有这些在事务中工作,因此不存在无法更新快照的风险。 它还处理将分区迁移到新服务器等问题(当这种情况发生时,会自动将快照KTable加载到本地RocksDB中)。

1
在事件源和消息队列(例如Apache Kafka)的概念中,您可以从事件源将事件流向Kafka Connect JDBC/Debezium CDC => Kafka。在DDD风格的事件源中,没有这样的消息队列。DDD的一种战术模式是聚合模式,它作为事务边界。DDD不关心如何保存聚合状态,通常人们使用基于状态的持久性与关系型或文档数据库。当应用基于事件的持久性时,我们需要将新事件作为一个事务存储到事件存储中,以便我们稍后检索这些事件以重构聚合状态。因此,要支持DDD风格的事件源,存储器需要能够通过聚合ID对事件进行索引,并且我们通常提到事件流的概念,其中这样的流由聚合标识符唯一标识,并且所有事件按顺序存储,因此该流表示单个聚合。
由于我们很少能够使用仅允许按其id检索单个实体的数据库生存,因此我们需要有一个地方将这些事件投影到其中,以便我们可以拥有可查询的存储。这就是您的图表在右侧显示的内容,称为实现视图。更常见的是,它被称为“读取端”,那里的模型被称为“读取模型”。这种存储不必保留聚合的快照。相反,读取模型的作用是以可以直接由UI / API消耗的方式表示系统状态,通常与域模型本身不匹配。
正如其中一篇答案中提到的那样,典型的命令处理程序流程是:
1.通过读取该聚合的所有事件来按ID加载一个聚合状态。它已经要求事件存储支持这种负载,而Kafka无法做到这一点。 2.调用域模型(聚合根方法)执行某些操作。 3.将新事件存储到聚合流中,全部或全部不存储。
如果您现在开始将事件写入存储并在其他地方发布它们,那么您将面临难以解决的两阶段提交问题。因此,我们通常更喜欢使用像EventStore这样的产品,它具有创建所有已编写事件的追赶订阅的功能。Kafka也支持这一点。还可以通过创建新的事件索引链接到现有事件来提高在存储中的效率,特别是如果有多个系统使用一个存储。在EventStore中,可以使用内部投影来完成这一点,也可以使用Kafka流来完成。
我认为,在写入和读取之间确实不需要任何消息传递系统。写入侧应允许您订阅事件源,从事件日志的任何位置开始,以便您可以构建读模型。
但是,Kafka仅适用于不使用聚合模式的系统,因为重要的是能够使用事件而不是快照作为真相来源,尽管这当然是可以商榷的。我建议考虑更改事件如何更改实体状态(例如修复错误),当您使用事件重构实体状态时,您将会很好,快照将保持不变,您需要应用纠正事件以修复所有快照。
我个人也更喜欢我的领域模型不与任何基础架构紧密耦合。事实上,我的领域模型对基础架构没有任何依赖。将快照逻辑带到Kafka流构建器中,我会立即被耦合起来,从我的角度来看,这不是最好的解决方案。

0

理论上你可以使用Kafka作为事件存储,但正如许多人在上面提到的那样,你将有几个限制,其中最大的限制是只能使用Kafka中的偏移量来读取事件,而不能使用其他标准。

因此,有一些框架处理问题的Event SourcingCQRS部分。

Kafka只是工具链的一部分,它为您提供了重放事件和背压机制的功能,以保护您免受过载。

如果您想看看所有这些如何配合,请参阅我的博客


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接