我有以下(轴突)聚集体:
@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
命令失败的最佳方法是什么?
自然,我可以先检查存储库,但这不会阻止竞争条件...
答案 0 :(得分:1)
如果汽车已经存在,确保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: *)。
事件存储通常支持比较和交换语义;通过精心设计一个查询来确定尾部指针的预期位置,并使用经过特殊编码的值来标识预期将创建流的情况(例如,事件存储支持{{3 }}语义)。
答案 1 :(得分:1)
但是,如果我发送另一个具有相同ID的CreateCar命令,则该命令将无法通过聚合验证(给定的ID已存在)。随后,它将仅触发一个新的CarCreated事件。这是骗人的。
Axon实际上会为您解决这个问题。聚合发布事件时,不会立即将其发布到其他组件。它在工作单元中上演,等待处理程序执行完成。 处理程序执行后,将调用许多“准备提交”处理程序。其中一个存储聚合(使用事件源时是无操作),另一个存储事件的发布(在事务范围内)。
取决于是否使用事件源,将Aggregate实例添加到持久性存储将失败(重复密钥),或者创建事件的发布将失败(重复的聚合标识符+序列号)。