活动采购-为什么要专门的活动商店?

时间:2019-06-24 01:44:54

标签: apache-kafka domain-driven-design cqrs event-sourcing

我正在尝试首次实现事件源/ CQRS / DDD,主要是出于学习目的,这里有一个事件存储和一个消息队列(例如Apache Kafka)的想法,并且您有事件从事件存储中流出=> Kafka Connect JDBC / Debezium CDC => Kafka。

我想知道为什么Kafka本身可以通过其主要功能和日志压缩或为永久存储配置日志保留功能来实现其目的,为什么需要一个单独的事件存储。我应该将事件存储在RDBMS之类的专用商店中以馈入Kafka还是直接将其馈入Kafka?

enter image description here

3 个答案:

答案 0 :(得分:2)

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

域驱动设计中的一种常见模式是拥有一个域模型,以确保持久存储中数据的完整性,即确保没有内部矛盾。

  

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

那么,如果我们想要一个没有内部矛盾的事件流,我们如何实现呢?一种方法是确保仅单个进程有权修改流。不幸的是,这给您带来了单点故障-流程终止,一切都结束了。

另一方面,如果您有多个进程更新同一个流,则存在并发写入,数据争用和引入矛盾的风险,因为一个编写者无法看到另一个编写者所做的事情。

借助RDBMS或事件存储,我们可以使用事务或比较和交换语义来解决此问题;如果进行了并发修改,则拒绝使用新事件扩展流的尝试。

此外,由于其DDD的传统,将耐久商店划分为许多非常细粒度的分区(也称为“聚合”)是很常见的。 One single shopping cart可能合理地具有四个流。

如果Kafka缺乏这些功能,那么它将成为事件存储的糟糕替代品。 KAFKA-2260已经开放了四年多了,因此我们似乎缺少第一个。从我从Kakfa文献中可以辨别的结果来看,对于细粒度的流也不满意(尽管自从我检查以来已经有一段时间了,也许情况已经改变了。)

另请参见:Jesper Hammarbäck关于这18个月前的文章,并得出与此处表达的结论相似的结论。

答案 1 :(得分:0)

Kafka可以用作DDD事件存储,但是由于缺少它的功能,这样做会带来一些麻烦。

人们用于聚合事件源的两个关键功能是:

  1. 通过读取 just 发生聚合的事件
  2. 来加载聚合
  3. 在为聚合同时写入新事件时,请确保只有一个写入器成功,以避免破坏聚合并破坏其不变式。

Kafka目前无法执行任何一项操作,因为1条失败,因为您通常需要每个集合 type 拥有一个流(它不会扩展到每个集合的一个流,而这不会无论如何都不一定要这么做),因此无法为一个聚合加载事件,并且由于https://issues.apache.org/jira/browse/KAFKA-2260尚未实现而导致2次失败。

因此,您必须以不需要功能1和2的方式编写系统。可以按照以下步骤进行操作:

  1. 与其直接调用命令处理程序,不如将它们写入 流。每个聚合类型都有一个命令流,由 集合ID(不需要永久保留)。这样可以确保您只处理一个 一次命令一个特定的聚合。
  2. 为所有聚合类型编写快照代码
  3. 在处理命令消息时,请执行以下操作:
    1. 加载汇总快照
    2. 验证命令
    3. 编写新事件(或返回失败)
    4. 将事件应用于汇总
    5. 保存新的聚合快照,包括事件流的当前流偏移量
    6. 将成功返回给客户(也许通过回复消息)

唯一的其他问题是处理失败(例如快照失败)。这可以在启动特定命令处理分区的过程中进行处理-它仅需要重播自上次快照成功以来的所有事件,并在恢复命令处理之前更新相应的快照。

Kafka Streams似乎具有使这一过程变得非常简单的功能-您拥有将KStream转换为KTable(包含快照,由聚合ID键入)的命令,以及事件KStream(以及可能包含响应的其他流) 。 Kafka允许所有这些事务以事务方式工作,因此没有更新快照失败的风险。它还将处理将分区迁移到新服务器等的情况(发生这种情况时,将快照KTable自动加载到本地RocksDB中)。

答案 2 :(得分:0)

  

这里有一个事件存储和一个消息队列(例如Apache Kafka)的想法,并且您有事件从事件存储中流出=> Kafka Connect JDBC / Debezium CDC => Kafka

在具有DDD风格的事件源的本质上,没有这样的消息队列空间。 DDD战术模式之一是聚合模式,它充当事务边界。 DDD不在乎聚合状态如何持久化,通常人们在关系数据库或文档数据库中使用基于状态的持久化。在应用基于事件的持久性时,我们需要将新事件作为一个事务存储到事件存储中,以便我们稍后可以检索这些事件以重建聚合状态。因此,为了支持DDD风格的事件源,商店需要能够通过聚合ID为事件建立索引,并且我们通常引用事件流的概念,其中,该流由聚合标识符唯一标识,并且所有事件是按顺序存储的,因此流表示单个聚合。

因为我们很少能够使用只能通过其ID检索单个实体的数据库,所以我们需要在某个地方可以将这些事件投影到其中,因此我们可以拥有一个可查询的商店。这就是您的图作为实体化视图在右侧显示的内容。通常,它称为 read side ,其中的模型称为 read-models 。这种存储不必保留聚合快照。相反,读取模型用于以一种可以由UI / API直接使用的方式表示系统状态的目的,并且通常与领域模型不匹配。

如此处答案之一所述,典型的命令处理程序流程为:

  1. 通过读取该聚合的所有事件,按ID加载一个聚合状态。事件存储已经需要支持Kafka无法做到的那种负载。
  2. 调用域模型(聚合根方法)以执行某些操作。
  3. 将新事件全部或全部存储到聚合流中。

如果现在开始将事件写入商店并将其发布到其他位置,则会遇到两阶段提交问题,这很难解决。因此,我们通常更喜欢使用EventStore之类的产品,该产品具有为所有书面事件创建追赶订阅的能力。卡夫卡也支持。能够在商店中创建新的事件索引并链接到现有事件,这也是有益的,特别是如果您有多个使用一个商店的系统。在EventStore中,可以使用内部投影来完成,也可以使用Kafka流来完成。

我认为您确实不需要在写和读端之间建立任何消息传递系统。写端应允许您从事件日志中的任何位置开始订阅事件提要,以便您可以构建读取模型。

但是,Kafka仅在不使用聚合模式的系统中工作,因为能够使用事件(而不是快照)作为事实的来源是必不可少的,尽管这当然是可以讨论的。我将研究一下更改事件如何更改实体状态(例如修复错误)的方式的可能性,以及当您使用事件来重建实体状态时,您会很好,快照将保持不变,并且将需要应用更正事件来修复所有快照。

我个人也希望不要与我的域模型中的任何基础架构紧密耦合。实际上,我的域模型对基础结构的依赖性为零。通过将快照逻辑引入Kafka流构建器,我将立即结识,这并不是最佳解决方案。