Concurrency CQR,单个聚合项的多个写入节点,同时保持并发性

Concurrency CQR,单个聚合项的多个写入节点,同时保持并发性,concurrency,message-queue,cqrs,Concurrency,Message Queue,Cqrs,假设我有一个命令来编辑一篇文章的单个条目,名为ArticleEditCommand 用户1根据文章的V1发出ArticleEditCommand 用户2基于相同的V1发出ArticleEditCommand 文章 如果我可以确保我的节点首先处理较旧的ArticleEditCommand命令,那么我可以确保来自用户2的命令将失败,因为用户1的命令将文章的版本更改为V2 但是,如果我有两个节点同时处理articleditcommand消息,即使这些命令将以正确的顺序从队列中获取,但由于CPU峰值

假设我有一个命令来编辑一篇文章的单个条目,名为
ArticleEditCommand

  • 用户1根据文章的V1发出
    ArticleEditCommand
  • 用户2基于相同的V1发出
    ArticleEditCommand
    文章
如果我可以确保我的节点首先处理较旧的
ArticleEditCommand
命令,那么我可以确保来自用户2的命令将失败,因为用户1的命令将文章的版本更改为V2

但是,如果我有两个节点同时处理
articleditcommand
消息,即使这些命令将以正确的顺序从队列中获取,但由于CPU峰值或类似情况,我无法保证节点将在第二个命令之前处理第一个命令。我可以使用sql事务来更新一篇version=expectedVersion的文章,并记录更改的记录数,但我的规则更复杂,不能只在sql中使用。我希望我的整个命令处理逻辑保证在修改同一篇文章的
ArticleEditCommand
消息之间是并发的

我不想在处理命令时锁定队列,因为拥有多个命令处理程序的目的是并发处理命令以实现可伸缩性。话虽如此,我并不介意这些命令被连续处理,但只针对一篇文章的单个实例/id。我不希望为一篇文章发送大量的
ArticleEditCommand
消息

说到这里,问题来了

是否有一种方法可以跨多个节点连续处理单个唯一对象(数据库记录)的命令,但同时处理所有其他命令(不同的数据库记录)?

或者,这是我自己造成的问题,因为我对CQR和并发性缺乏了解

这是消息代理通常解决的问题吗?例如Windows服务总线、MSMQ/NServiceBus等


编辑:我想我现在知道如何处理这个问题了。当用户2发出
articleditcommand
时,应该向用户抛出一个异常,让他们知道在对
articleditcommand
进行排队之前,该文章上有一个当前挂起的操作必须完成。这样,队列中就不会有两条
articleditcommand
消息影响同一篇文章。

首先让我说,如果您不希望发送大量
articleditcommand
消息,这听起来像是过早优化


在其他解决方案中,这个问题通常不是由消息代理解决的,而是由持久性实现实施的乐观锁定解决的。我不明白为什么SQL可以轻松处理的用于乐观锁定的简单
版本
字段与复杂的业务逻辑/更新相矛盾,也许您可以详细说明一下?

首先让我说,如果您不希望发送大量
articleditcommand
消息,这听起来像是过早的优化


在其他解决方案中,这个问题通常不是由消息代理解决的,而是由持久性实现实施的乐观锁定解决的。我不明白为什么SQL可以轻松处理的用于乐观锁定的简单
版本
字段与复杂的业务逻辑/更新相矛盾,也许您可以详细说明一下?

它实际上非常简单,我就是这么做的。基本上,它看起来像这样(伪代码)

和是我的CavemanTools通用库的一部分(可在Nuget上获得)

我们的想法是尝试正常操作,然后如果对象版本(sql中的rowversion/timestamp)发生了更改,我们将在等待几秒钟后再次重试整个操作。这正是TryUpdateEntity()方法所做的。您可以调整尝试之间的等待时间或重试操作的次数


如果您需要通知用户,然后忘记重试,只需直接捕获异常,然后告诉用户刷新或做其他事情。

实际上很简单,我就是这么做的。基本上,它看起来像这样(伪代码)

和是我的CavemanTools通用库的一部分(可在Nuget上获得)

我们的想法是尝试正常操作,然后如果对象版本(sql中的rowversion/timestamp)发生了更改,我们将在等待几秒钟后再次重试整个操作。这正是TryUpdateEntity()方法所做的。您可以调整尝试之间的等待时间或重试操作的次数


如果您需要通知用户,然后忘记重试,只需直接捕获异常,然后告诉用户进行刷新或其他操作。

基于分区的解决方案

通过基于对象的ID(例如,articleId模化节点数)路由传入命令来实现节点粘性,以确保User1和User2的命令最终位于同一节点上,然后连续处理这些命令。您可以选择一个接一个地处理所有命令,或者如果希望并行执行,可以将命令按ID、奇数/偶数、国家或类似方式进行分区

基于网格的解决方案

使用内存网格(如Hazelcast或Coherence)并使用分布式执行器服务()或类似服务来协调集群中的命令处理

不管怎样-在增加这种复杂性之前,您当然应该问问自己,如果接受User2的命令并且User1返回并发错误,这是否真的是个问题。只要User1的更改没有丢失,并且可以在文章刷新后重新应用,就可以了。

//message handler ModelTools.TryUpdateEntity( ()=>{ var entity= _repo.Get(myId); entity.Do(whateverCommand); _repo.Save(entity); } 10); //retry 10 times until giving up //repository long? _version; public MyObject Get(Guid id) { //query data and version _version=data.version; return data.ToMyObject(); } public void Save(MyObject data) { //update row in db where version=_version.Value if (rowsUpdated==0) { //things have changed since we've retrieved the object throw new NewerVersionExistsException(); } }