如何在水平扩展写入时避免并发问题?

时间:2015-03-08 19:10:29

标签: azure scalability sharding microservices horizontal-scaling

假设有一个工作服务从队列接收消息,从文档数据库中读取具有指定Id的产品,根据消息应用一些操作逻辑,最后将更新的产品写回数据库(a)

horizontally scaling writes

在处理不同产品时,可以安全地完成这项工作,因此我们可以横向扩展(b)。但是,如果多个服务实例在同一产品上运行,我们最终可能会遇到并发问题或数据库中的并发异常,在这种情况下我们应该应用一些重试逻辑(并且仍然可能再次失败,等等) 。

问题:我们如何避免这种情况?有没有办法可以确保两个实例不能在同一个产品上运行?

示例/用例:在线商店在productA,productB和productC上销售很快,一小时结束,数百名客户正在购买。对于每次购买,都会将消息排入队列(productId,numberOfItems,price)。 目标:我们如何运行三个工作服务实例,并确保productA的所有消息最终都在instanceA,productB到instanceB,productC到instanceC(导致没有并发问题)?

备注:我的服务是用C#编写的,作为辅助角色托管在Azure上,我使用Azure队列进行消息传递,我想将Mongo用于存储。此外,实体ID为GUID

更多的是关于技术/设计,所以如果你使用不同的工具来解决我仍然感兴趣的问题。

5 个答案:

答案 0 :(得分:1)

对于这种事情,我使用blob leases。基本上,我使用某个已知存储帐户中的实体ID创建一个blob。当worker 1选择实体时,它会尝试获取blob上的租约(如果它不存在,则创建blob本身)。如果两者都成功,那么我允许处理消息。之后总是发布租约。 如果我没有成功,我将消息转发回队列

我遵循史蒂夫·马克思最初描述的apporach http://blog.smarx.com/posts/managing-concurrency-in-windows-azure-with-leases尽管已经调整使用新的存储库

评论后编辑: 如果你有一个潜在的高消息率所有与同一实体交谈(正如你的推荐所暗示的那样),我会在某处重新设计你的方法..实体结构或消息结构。

例如:考虑CQRS设计模式并独立存储每条消息处理的变化。因此,产品实体现在是各种工作人员对实体所做的所有更改的集合,按顺序重新应用并重新水合成单个对象

答案 1 :(得分:1)

任何试图将负载分配到同一集合中的不同项目(如订单)的解决方案都注定要失败。原因是,如果你的交易率很高,你将不得不开始做以下事情之一:

  1. 让节点互相交谈(hey guys, are anyone working with this?
  2. 将ID生成划分为多个段(节点a创建ID 1-1000,节点B 1001-1999)等,然后让他们处理自己的段
  3. 将一个集合动态划分为多个段(让每个节点处理一个段。
  4. 那么这些方法有什么问题呢?

    第一种方法是简单地复制数据库中的事务。除非您可以花费大量时间来优化策略,否则最好依赖交易。

    后两个选项会降低性能,因为您必须在ID上动态路由消息,并在运行时更改策略以包括新插入的消息。它最终会失败。

    解决方案

    以下是您可以合并的两种解决方案。

    自动重试

    相反,你有一个从消息队列中读取的入口点。

    在其中你有这样的东西:

    while (true)
    {
        var message = queue.Read();
        Process(message);
    }
    

    为了获得非常简单的容错,你可以做的就是在失败时重试:

    while (true)
    {
        for (i = 0; i < 3; i++)
        {
           try
           {
                var message = queue.Read();
                Process(message);
                break; //exit for loop
           }
           catch (Exception ex)
           {
               //log
               //no throw = for loop runs the next attempt
           }
        }
    }
    

    您当然可以捕获数据库异常(或者说事务失败)来重放这些消息。

    微服务

    我知道,微服务是一个流行词。但在这种情况下,这是一个很好的解决方案。而不是使用处理所有消息的单片核心,而是将应用程序划分为较小的部分。或者在您的情况下,只需停用某些类型的消息的处理。

    如果您有五个运行应用程序的节点,您可以确保节点A收到与订单相关的消息,节点B接收与运输相关的消息等。

    通过这样做,您仍然可以水平扩展您的应用程序,您不会遇到任何冲突,只需要很少的工作(更多的消息队列并重新配置每个节点)。

答案 2 :(得分:1)

如果您希望始终使数据库保持最新并始终与已处理的单元保持一致,那么您在同一可变实体上有多个更新。

为了符合这一点,您需要序列化同一实体的更新。要么通过在生产者处对数据进行分区来执行此操作,要么在同一队列中累积实体的事件,要么使用分布式锁定锁定工作组中的实体,要么在数据库级别锁定。

你可以使用一个actor模型(在使用akka的java / scala世界中),它为每个连续处理它们的实体或实体组创建一个消息队列。

已更新 您可以尝试akka port to .nethere。 在这里,您可以找到一个很好的教程,其中包含有关使用akka in scala的示例。 但是对于一般原则,你应该更多地搜索[演员模型]。不过它有缺点。

最终涉及对数据进行分区以及为特定实体创建唯一的专业工作者(可以在发生故障时重用和/或重新启动)的能力。

答案 3 :(得分:0)

我假设您有办法安全地访问所有工作服务中的产品队列。鉴于此,避免冲突的一种简单方法是在主队列旁边使用每个产品的全局队列

// Queue[X] is the queue for product X
// QueueMain is the main queue 
DoWork(ProductType X)
{
  if (Queue[X].empty())
  {
    product = QueueMain().pop()
    if (product.type != X)
    {
      Queue[product.type].push(product) 
      return;
    }
  }else
  {
     product = Queue[X].pop()
  }

  //process product...
}

对队列的访问需要是原子的

答案 4 :(得分:-1)

1)我能想到的每一个高规模数据解决方案都内置了一些内容来处理这种冲突。详细信息取决于您对数据存储的最终选择。在传统的关系数据库的情况下,无需任何添加工作就可以实现这一目标。有关详细信息,请参阅您选择的技术文档。

2)了解您的数据模型和使用模式。适当地设计您的数据存储区。不要设计你不会拥有的规模。针对最常见的使用模式进行优化。

3)挑战你的假设。你真的来经常从多个角色改变同一个实体吗?有时答案是肯定的,但通常您只需创建一个类似于反映更新的新实体。 IE,采用日记/ logging方法而不是单实体方法。最终,单个实体的大量更新将永远无法扩展。