确保至少有一位消费者

时间:2017-03-21 16:27:51

标签: java python rabbitmq distributed-computing rabbitmq-exchange

TLDR;在消费者即时创建的主题交换和队列的上下文中,如果没有消费者消费该消息,如何重新传递消息/生产者通知?

我有以下组件:

  • 主要服务,生成文件。每个文件都有特定类别(例如 pictures.profile pictures.gallery
  • 一组工作者,使用文件并从中生成文本输出(例如文件的大小)

我目前只有一个RabbitMQ主题交换。

  • 制作人使用routing_key = file_category向交易所发送消息。
  • 每个消费者创建一个队列,并将交换绑定到此队列以获取一组路由键(例如图片。* videos.trending )。
  • 当消费者处理完文件时,会将结果推送到 processing_results 队列。

现在 - 这个工作正常,但它仍然有一个重大问题。目前,如果发布者发送的消息带有没有绑定使用者的路由密钥,则该消息将丢失。这是因为即使消费者创建的队列是持久的,一旦消费者断开连接,它就会被销毁,因为它对于这个消费者来说是唯一的

消费者代码(python):

channel.exchange_declare(exchange=exchange_name, type='topic', durable=True)
result = channel.queue_declare(exclusive = True, durable=True)
queue_name = result.method.queue

topics = [ "pictures.*", "videos.trending" ]
for topic in topics:
    channel.queue_bind(exchange=exchange_name, queue=queue_name, routing_key=topic)

channel.basic_consume(my_handler, queue=queue_name)
channel.start_consuming()

在我的用例中,不能接受在这种情况下丢失消息。

尝试解决方案

然而,"失去"如果通知生产者没有消费者收到消息,则消息变为可接受(在这种情况下,它可以稍后重新发送)。我发现强制字段可以提供帮助,因为AMQP的规范声明:

  

此标志告诉服务器如果无法将消息路由到队列,如何做出反应。如果设置了此标志,则服务器将返回带有Return方法的unroutable消息。

这确实有效 - 在制片人中,我能够注册ReturnListener

rabbitMq.confirmSelect();  

rabbitMq.addReturnListener( (int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) -> {
    log.info("A message was returned by the broker");
});
rabbitMq.basicPublish(exchangeName, "pictures.profile", true /* mandatory */, MessageProperties.PERSISTENT_TEXT_PLAIN, messageBytes);

如果使用路由密钥发送消息而没有消费者绑定,则会按预期打印A message was returned by the broker

现在,我还想知道消费者何时收到正确消息。所以我尝试注册ConfirmListener

rabbitMq.addConfirmListener(new ConfirmListener() {
    void handleAck(long deliveryTag, boolean multiple) throws IOException {
        log.info("ACK message {}, multiple = ", deliveryTag, multiple);
    }

    void handleNack(long deliveryTag, boolean multiple) throws IOException {
        log.info("NACK message {}, multiple = ", deliveryTag, multiple);
    }
});

这里的问题是ACK由经纪人发送,而不是由消费者本身发送。因此,当生产者发送带有路由密钥 K 的消息时:

  • 如果消费者绑定了此路由密钥,则代理只发送 ACK
  • 否则,经纪人发送 basic.return ,然后发送确认

参见文档:

  

对于不可路由的消息,一旦交换机验证了一条消息不会路由到任何队列(返回一个空的队列列表),代理就会发出确认。如果消息也作为必需消息发布,则basic.return将在basic.ack之前发送给客户端。否定确认(basic.nack)也是如此。

因此,虽然我的问题在理论上可以解决这个问题,但它会使逻辑知道消息是否被正确地消耗得非常复杂(特别是在多线程,数据库中的持久性等的上下文中):

send a message

on receive ACK:
    if no basic.return was received for this message
       the message was correctly consumed
    else
       the message wasn't correctly consumed

on receive basic.return
    the message wasn't correctly consumed

可能的其他解决方案

  • 为每个文件类别设置一个队列,即队列 pictures_profile,pictures_gallery,等。不好,因为它为消费者消除了很多灵活性

  • 有一个"响应超时"生产者的逻辑。制作人发送消息。它期待一个"答案"对于 processing_results 队列中的此消息。如果在X秒之后没有应答消息,则解决方案是重新发送消息。我不喜欢它,它会在制作人中创造一些额外的棘手逻辑。

  • 生成TTL为0的消息,并让生产者监听死信交换。这是取代'直接'的官方suggested solution。在RabbitMQ 3.0中删除了标记(请参阅删除"立即"标记)。根据死信交换的the docs,死信交换只能按队列配置。所以它不会在这里工作

  • [编辑]我看到的最后一个解决方案是让每个消费者创建一个持久队列,当他断开连接时不会销毁它,并让它听取它。示例:consumer1创建queue-consumer-1,该myExchange绑定到具有路由密钥abcd的{​​{1}}消息。我预见的问题是它意味着为每个消费者应用程序实例找到一个唯一的标识符(例如,它运行的机器的主机名)。

我很想得到一些意见 - 谢谢!

相关:

[edit]解决方案

如前所述,我最终实现了使用basic.return的东西。它实际上并不是那么棘手,你只需要确保生成消息的方法和处理基本返回的方法是同步的(或者如果不在同一个类中则有共享锁),否则你最终可能会交错的执行流程会破坏您的业务逻辑。

1 个答案:

答案 0 :(得分:2)

我认为an alternate exchange最适合您的用例,用于识别未路由消息。

  

每当与配置的AE交换无法将消息路由到任何队列时,它都会将消息发布到指定的AE。

基本上是在创建" main"交换,您为它配置备用交换。 对于引用的备用交换,我倾向于使用扇出,然后创建一个绑定到它的队列(notroutedq)。 这意味着任何未发布到至少一个绑定到" main"的队列的消息。交换将最终进入notroutedq

现在关于你的陈述:

  

因为即使消费者创建的队列是持久的,一旦消费者断开连接,它就会被销毁,因为它对这个消费者来说是唯一的。

似乎您已将自动删除的队列配置为true。 如果是这样, 如果断开连接,如您所述,队列将被销毁,队列中仍然存在的消息将丢失,而备用交换配置 则不包括这种情况。

从您的用例描述中不清楚您是否期望在某些情况下消息最终出现在多个队列中,似乎更多的情况是每种类型的处理预期一个队列(同时保持分组灵活)。如果队列拆分确实与处理类型有关,我看不到使用自动删除设置队列的好处,期望在您想要更改绑定时可能不需要进行任何清理维护

假设您可以使用持久队列,那么dead letter exchange(将再次使用扇出)与dlq的绑定将覆盖丢失的案例。

  • 未被备用交换覆盖
  • 您的processing_result队列已经处理的正确处理
  • 有问题的处理或者死信交换涵盖的处理时间太长,在这种情况下,添加标题后添加的标题甚至可以帮助确定要采取的行动类型