如何可靠地重放事件溯源事件?

How to replay Event Sourcing events reliably?

事件溯源的一大承诺是能够重播事件。当实体之间没有关系时(例如 blob 存储、用户配置文件)它工作得很好,但是当有重要的关系需要检查时如何快速重放?

例如:Product(id, name, quantity)Order(id, list of productIds)。如果我们有 CreateProduct 然后 CreateOrder 事件,那么它就会成功(产品在仓库中可用),它很容易实现,例如使用 Kafka(一个主题有 n1 个产品分区,另一个主题有 n2 个订单分区)。

在回放期间,一切都发生得更快,Kafka 可能会重新排序事件(例如 CreateOrder 然后 CreateProduct),这将给我们带来与最初不同的行为(CreateOrder 现在失败,因为产品尚不存在)。这是因为 Kafka 保证只在一个分区内的一个主题内排序。简单的解决方案是将所有内容放入一个分区的大主题中,但这将是完全不可扩展的,因为更大数据库的单线程重播至少需要几天时间。

有没有现有的、更好的快速重放相关实体的解决方案?或者,当我们需要检查数据库中的关系时,我们是否应该忘记事件溯源和事件重放,而重放只适用于不相关的数据?

作为事件溯源的实际需要,您需要能够为特定实体创建事件流,以便您可以应用事件处理程序来构建状态。对于 Kafka,除了实体太少以至于可以将整个主题分区分配给单个实体的事件之外,这需要通过分区进行线性扫描和过滤。因此,出于这个原因,虽然 Kafka 很可能成为任何 event-driven/event-based 系统的关键部分,用于中继服务发布的事件以供其他服务使用(此时,如果我们考虑事件与命令的二分法,我们是从消费服务的角度讨论命令),它不太适合事件存储的角色,事件存储的定义是它们能够快速为您提供特定实体的有序事件流。

最流行的专用事件存储可能是富有想象力的名称事件存储(至少部分是由于一些著名的事件溯源倡导者参与了其设计和实现)。或者,有像 Akka Persistence(带有 .Net 端口的 JVM)这样的 libraries/frameworks,它使用现有的数据库(例如关系 SQL 数据库、Cassandra、Mongo、Azure Cosmos 等)便于将其用作事件存储的方式。

事件溯源也作为一种实际需要往往会导致 CQRS(它们可以很好地结合在一起:事件溯源可以说是最简单的持久性模型,可以作为写入模型,而作为读取模型几乎没有用)。看到的典型模式是系统的命令处理组件强制执行约束,例如“产品在添加到购物车之前存在”(如何强制执行这些约束通常是使用的并发模型的问题:参与者模型具有很高的对这种方法的机械同情程度,但其他模型也是可能的)在将事件写入事件存储之前,然后可以假设从事件存储读回的事件在写入时是有效的(以后可以决定需要记录一个补偿事件)。事件存储中的事件可以投射到 Kafka 主题,以便与另一个服务通信(命令处理组件是事件的唯一真实来源)。

从其他服务的角度来看,如前所述,主题中的计划事件是命令(事件的隐式命令是“更新您的模型以说明此事件”)。从语义上讲,它们作为事件的出处意味着它们已经过验证并且是不可否认的(但是可以忽略它们)。如果需要进行某些模型验证,通常需要有意识地决定忽略该命令,或者等到收到另一个允许接受该命令的命令。

我想我找到了可扩展(多分区)事件溯源的解决方案:

  • 在 Kafka(或类似系统)中创建名为 messages
  • 的主题
  • 将用户分配给分区(例如 murmurHash(login) % partitionCount
  • 如果一段数据是可变的(例如ProductOrder),每个分区都应该包含自己的数据副本
  • 如果我们有我们仓库中有 256 件产品和 64 个分区,我们最初可以 'give' 每个分区 8 件,因此大多数 CreateOrder 事件将在不离开用户分区的情况下快速处理
  • 如果一个用户(一个分区)有时需要改变其他分区中的数据,它应该在那里发送消息:
    • 例如,对于 Product / Order 域,分区的工作方式类似于 Walmart/Tesco 一个国家/地区的商店,并且分区之间发送的消息 ('stores') 可以就像 CreateProductUpdateProductCreateOrderSendProductToMyPartitionProductSentToYourPartition
    • 消息将变成 'event',就好像它是由用户生成的一样
    • 回放时不应发送消息(已发送,无需发送两次)

这样即使 Kafka(或任何其他事件源系统)选择在分区之间重新排序消息,我们仍然没问题,因为我们不会在单线程之外读取任何数据 'island'.

编辑:正如@LeviRamsey 指出的那样,'single-threaded island' 基本上是演员模型,像 Akka 这样的框架可以让它更容易一些。

好吧,你还在想我们过去20年是怎么开发应用的,而不是我们未来应该怎么开发应用。有一些框架实际上非常适合未来的范式,其中之一就是上面提到的Akka but more importantly a sub component of it Akka FSM Finite State Machine,这是我们多年来在软件开发中忽略的一些概念,但未来似乎越来越基于事件和我们不能再忽视了。

那么这些将如何帮助你,Akka 是一个基于 Actor 概念的框架,每个 Actor 都是一个带有消息框的唯一实体,所以假设你有 ID 为 123456789 的 Order Actor,Order Id 的每个事件: 123456789 将使用此 Actor 进行处理,其消息将按照先进先出原则在其消息框中排序,因此您不再需要同步逻辑。但是你的系统中可能有数百万个 Order Actors,所以它们可以并行工作,当 Order Actor: 123456789 处理它的事件时,一个 Order Actor: 987654321 可以处理它自己的事件,所以有并行性和可扩展性。当您的 Kafka 保证 Key 123456789 和 987654321 的每条消息的顺序时,一切都是绿色的。

现在你可以问,有限状态机在哪里发挥作用,正如你提到的那样,问题出现了,当 addProduct 事件在 createOrder 事件到达之前到达时(在不同的 Kafka 主题上),此时,状态机将表现不同的是,当 Order Actor 处于 CREATED 状态或 INITIALISING 状态时,在 CREATED 状态下,它只会添加 Product,在 INITIALISING 状态下,它可能只会将其存储起来,直到 createOrder 事件到达。

这些概念在这个 video and if you want to see a practical example I have a blog for it and this one 中得到了很好的解释,以便更直接地潜水。