CQRS,单个聚合条目的多个写节点,同时保持并发

时间:2015-06-24 08:24:05

标签: concurrency message-queue cqrs

我们说我有一个命令来编辑一篇名为ArticleEditCommand的文章的单个条目。

  • 用户1根据文章的V1发出ArticleEditCommand
  • 用户2根据相同的V1发出ArticleEditCommand 制品。

如果我可以确保我的节点首先处理旧的ArticleEditCommand命令,我可以确定来自用户2的命令将失败,因为用户1的命令会将文章的版本更改为V2

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

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

话虽如此,这是一个问题。

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

或者,这是我自己创建的一个问题,因为他对CQRS和并发性缺乏了解吗?

这是消息经纪人通常已经解决的问题吗?如Windows Service Bus,MSMQ / NServiceBus等?

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

3 个答案:

答案 0 :(得分:0)

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

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

答案 1 :(得分:0)

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

//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();
    } 
 }

ModelTools.TryUpdateEntityNewerVersionExistsException是我的CavemanTools通用目标库的一部分(可在Nuget上找到)。

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

如果您需要通知用户,那么忘记重试,直接捕获异常然后告诉用户刷新或者其他什么。

答案 2 :(得分:0)

基于分区的解决方案

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

基于网格的解决方案

使用内存中的网格(例如Hazelcast或Coherence)并使用分布式执行程序服务(http://docs.hazelcast.org/docs/2.0/manual/html/ch09.html#DistributedExecution)或类似程序来协调整个群集中的命令处理。

无论如何 - 在添加这种复杂性之前,你当然应该问自己,如果User2的命令被接受并且User1收到并发错误,那真的是一个问题。只要User1的更改没有丢失并且可以在刷新文章后重新应用它就可以完全没问题。