C# 处理服务总线消息。完成()异常

C# 处理服务总线消息。完成()异常,c#,azure,servicebus,idempotent,C#,Azure,Servicebus,Idempotent,考虑这个场景,一个启用了消息重复数据消除的Azure服务总线,一个主题,一个订阅和一个订阅到该队列的应用程序 如何确保应用程序从队列接收消息一次且仅一次 以下是我在应用程序中用于接收消息的代码: public abstract class ServiceBusListener<T> : IServiceBusListener { private SubscriptionClient subscriptionClient; // ..... snip priv

考虑这个场景,一个启用了消息重复数据消除的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
            }
        }
    }
}
公共抽象类ServiceBusListener:IServiceBusListener { 私人认购客户认购客户; //……剪断 私有void ReceiveMessages() { message=this.subscriptionClient.Receive(TimeSpan.FromSeconds(5)); 如果(消息!=null) { T payload=message.GetBody(message); 尝试 { 销钉(有效载荷); message.Complete(); } 捕获(异常) { //消息。完成失败 } } } } 我所看到的问题是,如果
message.Complete()
由于任何原因失败,那么刚刚处理的消息将保留在Azure中订阅的队列中。再次调用
ReceiveMessages()
时,它将从队列中拾取相同的消息,应用程序将再次执行相同的工作

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

我能看到的唯一确保一次性交付到应用程序的方法是构建另一个队列,作为Azure服务总线和应用程序之间的中介。我相信这就是所谓的“持久客户端队列”


但是,我可以看到,对于许多使用Azure service bus的应用程序来说,这可能是一个潜在问题,所以持久的客户端队列是唯一的解决方案吗?

当您将消息出列时的默认行为称为“Peek Lock”它将锁定消息,以便在您处理消息时其他人无法获取消息,并在您提交时将其删除。如果您未能提交,它将解锁,因此可以再次提取它。这可能就是你正在经历的您可以将行为更改为使用“接收并删除”,这将在您接收到它进行处理后立即将其从队列中删除。


如果包含逻辑来检测消息是否已成功处理或消息处理达到的阶段,则可以继续使用单个订阅

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

如果这是不可能的,那么我应用的另一个解决方案是更新表存储以记录处理消息的进度。拾取消息时,将检查该表,以查看是否已完成任何阶段。这允许消息从先前到达的阶段继续

您概述的场景最有可能的原因是完成工作所花费的时间超过了对消息的锁定。可以将消息锁定超时调整为安全超过预期工作时间的值。 如果您能够跟踪处理消息锁过期所花费的时间,那么也可以在处理程序中对消息调用RenewLock

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


很难给出一个明确的答案而不知道你的DOWORK()涉及什么,但是我会把上面提到的一个或多个组合作为一个更好的解决方案。

< P>我在一个大型的Azure平台上有类似的挑战。我使用补偿事务模式()和事件源模式()体现的概念的逻辑组合。具体如何合并这些概念会有所不同,但最终,您可能需要根据自己的“回滚”逻辑进行规划,或者检测到前一个过程100%成功完成,减去消息的删除。如果您可以提前检查某些内容,您将知道某条消息根本没有被删除,然后完成它并继续。这张“支票”有多贵,这可能是个坏主意。您甚至可以“创建”一个人工的最后一步,如向数据库添加一行,该步骤仅在DoWork到达末尾时运行。然后,您可以在处理任何其他消息之前检查该行

在我看来,最好的方法是确保DoWork()中的所有步骤都检查是否存在已执行的工作(如果可能)。例如,如果正在创建DB表,请运行“如果不存在(从信息模式中选择表名称…”。在这种情况下,即使在不太可能发生这种情况的情况下,也可以安全地再次处理消息

我使用的其他方法是存储前X条消息(即10000条消息)的MessageID(每条消息上的顺序bigint),然后在继续处理消息之前检查它们是否存在(不在)。没有您认为的昂贵,而且非常安全。如果找到,只需完成()消息并继续。在其他情况下,我使用“开始”类型状态更新消息(在某些队列类型中内联,在其他队列类型中保留),然后继续。如果您阅读了一条消息,并且该消息已设置为“开始”,则您知道某个消息失败或已执行