【问题标题】:Best practice for preventing duplicate AggregateCreated events防止重复 AggregateCreated 事件的最佳实践
【发布时间】:2018-11-07 10:23:48
【问题描述】:

我有以下(轴突)聚合体:

@Aggregate
@NoArgsConstructor
public class Car{
  @AggregateIdentifier
  private String id;

  @CommandHandler
  public Car(CreateCar command){
    apply( new CarCreated(command.getId()) );
  }

  @EventSourcingHandler
  public void carCreated(CarCreated event) {
    this.id = event.getId();
  }

}

我可以通过提交带有特定 ID 的 CreateCar 命令来创建汽车,从而引发 CarCreated 事件。太好了。

但是,如果我发送另一个具有相同 ID 的 CreateCar 命令,则该命令无法通过聚合验证(给定 ID 已经存在)。随后,它将简单地触发一个新的CarCreated 事件。这是一个谎言。

如果汽车已经存在,确保CreateCar 命令失败的最佳方法是什么?

当然,我可以先检查存储库,但这不会阻止竞争条件...

【问题讨论】:

  • 使用 UUID 或在数据库中生成 id
  • 这如何防止发生两个 CarCreated 事件?
  • 不能将具有相同聚合标识符的相同创建事件保存在事件存储中,因此该事务应该在数据库级别失败,并被回滚。我怀疑您正在使用 UUID 作为聚合标识符,并且实际上希望聚合对另一个标识符也有唯一约束。在这种情况下,就轴突而言,您正在创建一个新的唯一聚合,它恰好具有与另一个相同的有效负载。您必须使用锁定解决此问题并读取查询表。
  • 这通常使用乐观并发来处理。当向您的存储技术发送事件时,您应该将预期的事件索引与事件一起发送。对于您创建的事件,该事件将为 0(对于基于 0 的系统)。您的存储技术有望支持这一点,并将检查索引为 0 的事件是否尚未存储,否则会失败。您可以通过补充聚合获得当前事件指数。看,当您获得聚合的事件时(在处理命令之前),您只需计算事件的数量并将其用作预期的事件索引。

标签: java cqrs axon


【解决方案1】:

但是,如果我发送另一个具有相同 ID 的 CreateCar 命令,则聚合无法验证该命令(即给定 ID 已存在)。随后,它将简单地触发一个新的 CarCreated 事件。这是谎言。

Axon 实际上会为您解决这个问题。当聚合发布事件时,它不会立即发布到其他组件。它在工作单元中暂存,等待处理程序执行完成。 处理程序执行后,会调用许多“准备提交”处理程序。其中一个存储聚合(在使用事件溯源时是无操作的),另一个是事件的发布(在事务范围内)。

根据您是否使用事件溯源,将聚合实例添加到持久存储将失败(重复键),或者创建事件的发布将失败(重复聚合标识符 + 序列号)。

【讨论】:

【解决方案2】:

如果汽车已经存在,确保 CreateCar 命令失败的最佳方法是什么?当然,我可以先检查存储库,但这不会阻止竞争条件...

没有魔法。

如果您要避免激烈的写入,那么您需要获取数据存储上的锁,或者您需要具有compare and swap 语义的数据存储。

使用锁,您可以保证在您读取存储中的数据和随后的写入之间不会发生冲突更新。

lock = lock_for_id id
lock.acquire
Try:
    Option[Car] root = repository.load id
    switch root {

        case None:
            Car car = createCar ...
            repository.store car  

        case Some(car):
            // deal with the fact that the car has already been created   

    }
Finally:
    lock.release

您希望为每个聚合都设置一个锁,但创建锁与创建聚合的条件相同。因此,您最终可能会使用 coarse grained lock 之类的东西来限制对操作的访问。

通过比较和交换,您可以将争用管理推向数据存储。您发送的不是PUT,而是conditional PUT。

    Option[Car] root = repository.load id
    switch root {

        case None:
            Car car = createCar ...
            repository.replace car None

        case Some(car):
            // deal with the fact that the car has already been created   

    }

我们不再需要锁,因为我们正在为存储精确地描述需要满足的前提条件(例如If-None-Match: *)。

事件存储通常支持比较和交换语义;将新事件“附加”到流上是通过制作一个标识尾指针的预期位置的查询来完成的,并使用特殊编码的值来标识预期创建流的情况(例如,事件存储支持ExpectedVersion.NoStream语义)。

【讨论】:

    猜你喜欢
    • 2023-03-17
    • 2022-01-18
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 2011-04-16
    • 2010-11-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多