【问题标题】:Event Sourcing: concurrently creating conflicting events事件溯源:同时创建冲突事件
【发布时间】:2017-05-19 05:44:13
【问题描述】:

我正在尝试使用 Kafka 实现事件溯源系统,但遇到了以下问题。在新用户注册期间,我想检查用户提供的用户名是否已被使用。但是,请考虑两个用户尝试同时注册并提供相同用户名的情况。

根据我对 ES 工作原理的理解,处理注册请求的控制器将检查请求是否有效,然后将新事件(例如 NewUser)发送到 Kafka,最后选择该事件由另一个控制器将其保存在物化视图中(例如 Postgres DB)。问题是请求的验证是针对物化视图完成的,但它的实际持久性发生在以后。因此,由于 2 个请求是并行处理的(由不同的服务实例),它们可能都通过了验证,从而产生 2 个 NewUser 消息。但是,当第二个控制器尝试将这 2 个 NewUser 消息持久保存在数据库中时,保存第二个事件将失败,因为违反了用户名的唯一性约束。

关于如何解决这个问题的任何想法?

谢谢。

更新:

特别是,我想验证以下是否是解决问题的公认方法:

  1. 使用用户名作为 userId(限制性)
  2. 向按用户名和验证时分区的主题发送事件 完成发送事件到另一个主题

【问题讨论】:

    标签: concurrency apache-kafka event-sourcing


    【解决方案1】:

    在您有约束的大多数情况下,针对具体化视图的初始验证是不够的。总有一些相关事件尚未实现。有两种主要的concurrency control 方法可以确保生成正确的结果:

    1.悲观的方法: 如果您想在发布事件之前验证约束,则需要锁定相关资源(实体、聚合或数据集)。锁定意味着您的服务不能在这些资源上发布事件。在此之后,要获取数据的当前状态:

    • 您可以等到锁定之前发布的所有事件都实现。
    • 您可以从数据库中读取当前状态,并在单独的进程中对其应用事件。

    2。乐观的方法: 在这种方法中,您在发布事件后执行验证。为此,您需要实施反馈机制。消费事件和执行验证的进程应该能够发布验证结果。您可以在可能的情况下在内存中执行验证。否则,您可以依赖您的物化数据存储。

    Martin Kleppman 在herehis book 中谈到了完全相同问题的两步解决方案。在此解决方案中,有两个主题:“声明”和“注册”。首先,您发布声明以获取用户名,然后尝试将其写入数据库,最后将结果发布到注册主题。在概念层面,它遵循您提到的第二种方法中的相同步骤。在验证步骤中,避免了依赖数据库实现验证逻辑和将二级索引保存在内存中。

    【讨论】:

    • 感谢您的反馈。悲观的方法似乎非常有限,所以对我来说这不是一个选择。我用乐观方法遇到的困难是事件应该描述已经发生的事情。因此,在我的示例中,如果发送了与新用户帐户关联的事件,则认为已创建新帐户(例如,其他服务将更新其状态等)。那么创建验证错误事件会产生什么结果呢?
    • 是的,根据最广泛的事件溯源定义,事件描述了已经发生的事情。但还有一件事。这些系统通常被描述为最终一致系统。这意味着系统可能在某个时间点处于不一致的状态。在这种系统中,典型的方法是编写 compensating services 来撤消操作。在您的示例中,如果您的系统最终保持一致,则补偿服务可以在检测到违规后为第二个帐户发布取消/删除事件。
    • Kleppman 谈到了完全相同问题的替代解决方案here。在此解决方案中,有两个主题:“声明”和“注册”。首先,您发布声明以获取用户名,这意味着您的事件并不是真正发生的事情。您尝试将其写入数据库,然后将结果发布到注册主题。与我之前谈到的方法相比,这种方法立即一致。
    • 注意是立即一致的,因为事件引起的突变只是一次原子写入。如果您真的需要在不同的数据存储之间立即保持一致性,那就另当别论了。
    • 谢谢!我也浏览了这本书,其中也提到了这一点。这基本上是选项 b) 在我对 VoiceOfUnreason 发表的评论的回复中:“将事件发送到按用户名分区的主题,并在完成验证后将事件发送到另一个主题”。因此,最好验证 Kleppman 是否认为这是最佳实践。如果您可以更新您的答案以提及这一点,我会接受它。再次感谢!
    【解决方案2】:

    在新用户注册期间,我想检查用户提供的用户名是否已被使用。

    您可能想在 Set Validation 上查看 Greg Young 的文章。

    根据我对 ES 工作原理的理解,处理注册请求的控制器将检查请求是否有效,然后它将一个新事件(例如 NewUser)发送到 Kafka,最后该事件将被另一个控制器,它将把它保存在物化视图中(例如 Postgres DB)。

    这与通常的安排有点不同。 (您可能还想查看 Greg 在 polyglot data 上的演讲。)

    假设我们从两个作家开始;没关系,但如果有一个单一的事实,那么你将需要在某个地方进行同步。

    通常的安排是使用乐观并发的一种形式;处理请求时,您保留原始状态的副本,然后进行计算,最后将“replace(originalState,newState)”发送给记录簿。

    所以在这一点上,我们有两个写入记录在竞争中

    replace(red,green)
    replace(red,blue)
    

    在记录簿中,写入是按顺序处理的。

    [...,replace(red,blue)...,replace(red,green)]
    

    因此,当记录簿处理replace(red,blue) 时,它会检查是的,状态当前为红色,并切换为蓝色。后来,当记录簿尝试处理replace(red,green)时,记录簿执行检查,由于状态不再是红色而失败。

    所以一个写入成功,另一个失败;后者可以向外传播失败,或者重试,或者......,这取决于所讨论的特定机制。当然,重试应该意味着重新加载“原始状态”,此时模型会发现之前的某些编辑已经声明了用户名。

    关于如何解决这个问题的任何想法?

    每个流单个写入器通过消除模型的多个内存副本引入的歧义,使问题的其余部分变得非常简单。

    使用同步写入持久存储的多个写入器可能是最常见的设计。它需要一个事件存储来理解写入流中特定位置的想法——也就是“预期版本”。

    您可以执行异步写入,然后开始执行其他工作,直到您收到写入成功的确认(或没有,或直到您超时,或)......

    没有什么神奇的——如果你想要唯一性(或任何其他类型的不变执行,就此而言),那么每个人都需要就一个单一的权威达成一致,而任何想要提出改变的人都不知道是否它已被接受而没有得到当局的回复,需要为被拒绝的提案做好准备。

    (注意:这应该不足为奇——如果您使用传统设计并将当前状态存储在 RDBMS 中,那么您的权限将是数据库中的用户表,对用户名列具有唯一性约束,并且竞争将在两个插入语句之间尝试首先完成它们的事务....)

    【讨论】:

    • 我知道处理这种竞争条件的方法是强制执行某种同步。这肯定没有什么魔力。我的问题是如何专门用 Kafka 来实现这一点。因为我的主题是由 userId 分区的,所以我可以想到 2 个选项:a)使用用户名作为 userId,b)将事件发送到由用户名分区的主题,并在验证完成后将事件发送到另一个主题。这两个都不理想(出于不同的原因),并且不能解决对超过 1 个不变量强制唯一性的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-06
    • 2020-05-27
    • 2021-06-24
    相关资源
    最近更新 更多