处理服务总线Message.Complete()异常

时间:2015-06-19 15:43:33

标签: c# azure servicebus idempotent

考虑这个场景,一个启用了消息重复数据删除的Azure服务总线,它有一个主题,一个订阅和一个订阅该队列的应用程序。

如何确保应用程序只从队列接收一次消息?

以下是我在我的应用程序中用于接收消息的代码:

public abstract class ServiceBusListener<T> : IServiceBusListener
{
    private SubscriptionClient subscriptionClient;
    // ..... snip

    private void ReceiveMessages()
    {
        message = this.subscriptionClient.Receive(TimeSpan.FromSeconds(5));

        if (message != null)
        {
            T payload = message.GetBody<T>(message);                                    

            try
            {
                DoWork(payload);

                message.Complete();
            }
            catch (Exception exception)
            {
                // message.Complete failed
            }
        }
    }
}

我预见到的问题是,如果message.Complete()因任何原因失败,那么刚刚处理过的消息将保留在Azure中的订阅队列中。当再次调用ReceiveMessages()时,它将从队列中获取相同的消息,应用程序将再次执行相同的工作。

虽然最好的解决方案是使用幂等域逻辑(DoWork(payload)),但在这种情况下编写这将非常困难。

我能看到的唯一方法是确保一次且仅一次传递到应用程序是通过构建另一个队列来充当Azure服务总线和应用程序之间的中介。我相信这被称为“持久的客户端队列”。

但是我可以看到,对于许多使用Azure服务总线的应用程序来说,这将是一个潜在的问题,因此持久的客户端队列是唯一的解决方案吗?

3 个答案:

答案 0 :(得分:3)

当您将消息出列时的默认行为称为“Peek-Lock”,它将锁定消息,以便在您处理消息时没有其他人可以获取它,并在您提交时将其删除。如果你没有提交它会解锁,所以它可以再次被拿起。这可能就是您所经历的。 您可以更改行为以使用“接收和删除”,一旦您收到进行处理,就会将其从队列中删除。 https://msdn.microsoft.com/en-us/library/azure/hh780770.aspx

https://azure.microsoft.com/en-us/documentation/articles/service-bus-dotnet-how-to-use-topics-subscriptions/#how-to-receive-messages-from-a-subscription

答案 1 :(得分:2)

我在我负责的大型Azure平台中遇到了类似的挑战。我使用了补偿交易模式(https://msdn.microsoft.com/en-us/library/dn589804.aspx)和事件源模式(https://msdn.microsoft.com/en-us/library/dn589792.aspx)所体现的概念的逻辑组合。确切地说,如何整合这些概念会有所不同,但最终,您可能需要自行规划&#34;回滚&#34;逻辑,或检测到先前的过程100%成功完成减去消息的删除。如果您可以预先检查某些内容,则会知道某条消息根本未被删除,然后完成并继续。检查&#34;检查&#34;可能会让这个坏主意。你甚至可以创造&#34;一个人为的最后一步,比如向DB添加一行,只有在DoWork到达结束时才会运行。然后,您可以在处理任何其他消息之前检查该行。

IMO,最好的方法是确保DoWork()中的所有步骤都检查工作是否已经执行(如果可能)。例如,如果它正在创建一个数据库表,则运行一个&#34; IF NOT EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA ......&#34;。在这种情况下,即使在不太可能的情况下发生这种情况,它也会#&# 39;可以安全地再次处理消息。

我使用的其他方法是存储先前X消息(即10,000)的MessageID(每条消息的连续bigint),然后在继续处理之前检查它们的存在(NOT IN)信息。没有您想象的那么昂贵且非常安全。如果找到,只需完成()消息并继续。在其他情况下,我使用&#34;开始&#34;更新消息。类型状态(在某些队列类型中内联,在其他队列中保留在其他位置),然后继续。如果您阅读了一条消息并且已经设置为&#34;已启动&#34;,则您知道某些内容已失败或未正确清除。

对不起,这不是一个明确的答案,但有很多考虑因素。

最诚挚的问候......

答案 2 :(得分:1)

如果您包含用于检测邮件是否已成功处理的逻辑,或者您已在邮件处理中达到的阶段,则可以继续使用单个订阅。

例如,我使用服务总线消息将来自外部支付系统的付款插入CRM系统。在插入之前,消息处理逻辑首先检查CRM中是否已存在支付(使用与支付相关联的唯一ID)。这是必需的,因为偶尔付款会成功添加到CRM但不会报告回来(超时或连接)。在接收邮件时使用接收/删除意味着付款可能会丢失,而不是检查已存在的付款是否会导致重复付款。

如果无法做到这一点,那么我应用的另一个解决方案是更新表存储以记录处理消息的进度。在选择消息时,将检查表以查看是否已完成任何阶段。这允许消息从之前到达的阶段继续。

您概述的最可能的原因是,DoWork所用的时间超过了对邮件的锁定。可以将消息锁定超时调整为安全地超过预期的DoWork时间段的值。 如果您能够跟踪处理消息锁定到期所需的时间,也可以在处理程序内的消息上调用RenewLock。

也许我误解了第二个队列的设计原则,但似乎这就像你概述的原始场景一样容易受到攻击。

如果不知道你的DoWork()涉及什么,很难给出明确的答案,但我会考虑将上述内容中的一个或组合作为更好的解决方案。