【问题标题】:Version number in event sourcing aggregate?事件溯源聚合中的版本号?
【发布时间】:2019-07-16 00:00:08
【问题描述】:

我正在构建微服务。我的微服务之一是使用 CQRS 和事件溯源。系统中引发了集成事件,我将聚合保存在事件存储中,同时更新我的​​读取模型。

我的问题是,为什么我们在针对该聚合更新事件流时需要聚合版本?我读到我们需要这个以保持一致性,并且事件将按顺序重播,我们需要在保存之前检查版本(https://blog.leifbattermann.de/2017/04/21/12-things-you-should-know-about-event-sourcing/)我仍然无法解决这个问题,因为事件是按顺序引发和保存的,所以我真的需要具体的例子来了解我们从版本中获得了什么好处以及为什么我们甚至需要它们。

非常感谢,

伊姆兰

【问题讨论】:

标签: events design-patterns microservices cqrs event-sourcing


【解决方案1】:

让我描述一个聚合版本有用的案例:

在我们的reSove framework 聚合版本中用于乐观并发控制。

我会举例说明。假设InventoryItem 聚合接受命令AddItemsOrderItemsAddItems 增加库存商品数量,OrderItems - 减少。 假设您有一个 InventoryItem 聚合 #123,其中包含一个事件 - ITEMS_ADDED,数量为 5。聚合 #123 状态表示有 5 件库存。

因此,您的 UI 向用户显示有 5 件商品有货。用户 A 决定订购 3 件商品,用户 B - 4 件商品。两者几乎同时发出 OrderItems 命令,假设用户 A 先是几毫秒。

现在,如果您在内存中有一个聚合 #123 的单个实例,在单个线程中,您没有问题 - 来自用户 A 的第一个命令将成功,将应用事件,状态说数量为 2,所以来自用户 B 的第二个命令会失败。

在分布式或无服务器系统中,来自 A 和 B 的命令将位于不同的进程中,如果我们不使用某些并发控制,这两个命令都会成功并导致聚合进入不正确的状态。有几种方法可以做到这一点 - 悲观锁定、命令队列、聚合存储库或乐观锁定。

乐观锁定似乎是最简单最实用的解决方案:

我们说每个聚合都有一个版本 - 其流中的事件数。所以我们的聚合 #123 有版本 1。

当聚合发出事件时,此事件数据具有聚合版本。在我们的例子中,来自用户 A 和 B 的 ITEMS_ORDERED 事件的事件聚合版本为 2。显然,聚合事件的版本应该是按顺序增加的。所以我们需要做的只是设置一个数据库约束,元组{aggregateId, aggregateVersion} 在写入事件存储时应该是唯一的。

让我们看看我们的示例如何在具有乐观并发控制的分布式系统中工作:

  • 用户 A 为聚合 #123 发出命令 OrderItem

  • 聚合 #123 从事件 {version 1, quantity 5} 恢复

  • 用户 B 为聚合 #123 发出命令 OrderItem

  • 另一个聚合 #123 实例从事件中恢复(版本 1,数量 5)

  • 用户A的聚合实例执行命令,成功,事件ITEMS_ORDERED {aggregateId 123, version 2}写入事件存储。

  • 用户 B 的聚合实例执行命令,成功,事件 ITEMS_ORDERED {aggregateId 123, version 2} 尝试将其写入事件存储并失败并出现并发异常。

  • 在用户 B 的此类异常命令处理程序上只是重复整个过程 - 然后聚合 #123 将处于 {version 2, quantity 2} 状态并且命令将被正确执行。

我希望这可以清除聚合版本有用的情况。

【讨论】:

  • 感谢罗曼。正是我在寻找。当我的命令成功时,我还会更新我的读取模型吗?在同一示例中,发生并发异常时如何管理读取模型。由于并发问题可能会多次出现,因此命令处理程序会继续尝试多长时间?
  • 读取模型是从事件存储构建的,因此您只需在事件成功保存后应用事件,而不是在此之前。
  • 重试:除非您有一个每秒接收数千条命令的聚合(这将是一个设计缺陷),否则并发异常非常罕见。在我的示例中,用户 B 应该在用户 A 之后但在用户 A 的事件被保存之前发送命令 - 在毫秒内。因此,如果 1-2 次重试没有帮助 - 您的设计中可能存在错误。
  • @RomanEremin 谢谢你这个惊人的例子和解释,我研究了一整天,然后我找到了这个回复,我必须说它让一切都很清楚。
【解决方案2】:

是的,这是正确的。您需要版本或序列号以保持一致性。

你想要的两件事:

  1. 正确排序
    通常事件本质上是幂等的,因为在分布式系统中,幂等消息或事件更容易处理。幂等消息是即使多次应用也会给出相同结果的消息。用固定值(比如 1)更新寄存器是幂等的,但将计数器递增 1 则不是。在分布式系统中,当 A 向 B 发送消息时,B 会确认 A。但是如果 B 使用该消息并且由于某些网络错误导致对 A 的确认丢失,A 不知道 B 是否收到消息,因此它会发送消息再次。现在 B 再次应用消息,如果消息不是幂等的,则最终状态将出错。所以,你想要幂等消息。但是如果你没有按照它们产生的顺序应用这些幂等消息,你的状态就会再次出错。可以使用版本 ID 或序列来实现此排序。如果您的事件存储是一个 RDBMS,则您无法在没有任何类似排序键的情况下对事件进行排序。在 Kafka 中,您也有偏移 id,并且客户端会跟踪它所消耗的偏移量

  2. 重复数据删除
    其次,如果你的消息不是幂等的怎么办?或者,如果您的消息是幂等的,但消费者以非确定性方式调用一些外部服务怎么办。在这种情况下,您需要一个完全一次的语义,因为如果您两次应用相同的消息,您的状态将是错误的。这里还需要版本 ID 或序列号。如果在消费者端,您跟踪已处理的版本 id,则可以根据 id 进行重复数据删除。在 Kafka 中,您可能希望将偏移 id 存储在消费者端

基于 cmets 的进一步说明:

相关文章的作者假设 RDBMS 作为事件存储。版本 ID 或事件序列预计由生产者生成。因此,在您的示例中,“已交付”事件的顺序将高于“运输中”事件。

当您想要并行处理事件时,就会出现问题。如果一个消费者收到“已交付”事件而另一个消费者收到“运输中”事件怎么办?显然,您必须确保特定订单的所有事件都由同一消费者处理。在 Kafka 中,您可以通过选择 order id 作为分区键来解决此问题。由于一个分区将仅由一个消费者处理,因此您知道您将始终在交付之前获得“传输中”。但是多个订单将分布在同一消费者组内的不同消费者中,因此您需要进行并行处理。

关于聚合 id,我认为这是 Kafka 中主题的同义词。由于作者假设 RDBMS 存储,他需要一些标识符来隔离不同类别的消息。您可以通过在 Kafka 中创建单独的主题以及每个聚合的消费者组来做到这一点。

【讨论】:

  • 感谢 saptarshi。假设我有运输微服务。我正在记录从预订到运输并最终交付的订单事件存储中的每个状态变化。假设我在运输途中收到了交付事件。我该如何处理?如果事件未按顺序传递,我如何更新我的读取模型。我正在针对我希望有序的订单聚合和事件流创建事件流。你能解释一下那部分吗?还有两个版本事件版本和聚合版本。它们是如何结合在一起的
猜你喜欢
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多