RabbitMQ - 消息传递的顺序

87

我需要为我的新项目选择一个新的队列代理。这一次,我需要一个可扩展的队列,支持发布/订阅,并且必须保持消息顺序。

我看了Alexis的评论: 他写道:

"实际上,我们认为RabbitMQ比Kafka提供更强的排序"

我读了rabbitmq文档中关于消息排序的部分:

"可以使用带有重新排队参数(basic.recover、basic.reject和basic.nack)的AMQP方法或由于通道关闭而保留未确认消息的方式将消息返回到队列...从2.7.0版本开始,如果队列有多个订阅者,个别消费者仍然可以观察到无序的消息。这是由于其他订阅者重新排队消息的操作。从队列的角度来看,消息始终按照发布顺序保持。"

如果我需要按照它们的顺序处理消息,我只能使用每个消费者的专用队列来使用RabbitMQ吗?

目前RabbitMQ是否仍被认为是有序消息排队的好解决方案?

4个回答

193

好的,让我们更仔细地看一下您上面描述的情况。我认为立即在问题段落前粘贴文档以提供上下文非常重要:

AMQP 0-9-1核心规范的第4.7节解释了保证排序的条件:在一个通道中发布的消息,通过一个交换和一个队列和一个出站通道将按照发送顺序接收到。从RabbitMQ 2.7.0版本开始,它提供了更强的保证。

使用带有重新排队参数(basic.recover,basic.reject和basic.nack)的AMQP方法或由于持有未确认消息而关闭通道会导致消息返回到队列。任何这些情况都会导致消息在早于2.7.0版本的RabbitMQ版本中重新加入队列的末尾。从RabbitMQ 2.7.0版本开始,消息始终按发布顺序保留在队列中,即使存在重新排队或通道关闭也是如此。 (强调添加)

因此,很明显,从2.7.0版本开始,RabbitMQ在消息排序方面比原始的AMQP规范做出了相当大的改进。

使用多个(并行)消费者时,无法保证处理顺序。
第三段(被粘贴到问题中)继续给出免责声明,我来描述一下:“如果队列中有多个处理器,则不再保证以正确的顺序处理消息。”这里他们所说的只是RabbitMQ不能违背数学定律。

考虑银行的客户队伍。这家特殊的银行以按照进入银行的顺序为客户提供帮助而自豪。客户排队,由3个可用的出纳员中的下一个服务。

今天早上,碰巧三个出纳员同时有空,接下来的三位客户分别前来办理业务。突然间,排在第一个的出纳员突发重病,无法为排队的第一位顾客服务。这时,第二个出纳员已经完成了第二位顾客的服务,而第三个出纳员已经开始为第三位顾客服务。
现在,可能会出现以下两种情况:(1)第一位顾客可以回到队伍头部;(2)第一位顾客可以抢占第三位顾客的位置,导致第三个出纳员停止为第三位顾客服务,转而为第一位顾客服务。这种抢占逻辑不被 RabbitMQ 或其他我所知的任何消息代理支持。在任意情况下,第一位顾客实际上都不会首先得到帮助——第二位顾客会得到帮助,因为他能够幸运地获得一个好的、快速的出纳员。只有让一个出纳员逐一为顾客服务,才能保证按顺序为顾客提供服务,但这将对银行的客户服务造成重大问题。
在您拥有多个消费者的情况下,无法保证每种情况下都按顺序处理消息。无论您拥有多个队列、多个独占消费者、不同的代理等,都无法事先保证按顺序为多个消费者回答消息。但是,RabbitMQ 将尽最大努力进行排序。

3
有没有一种方法可以配置 RabbitMQ,使其重新排队的消息排在队列末尾而不是队列前面? - Ryan Walls
7
@Ryan: 不行,但有个变通方法:你可以克隆消息并将其发布到同一队列中,就像完全新的消息一样,然后它会排到队列的末尾。在这种情况下,消息的属性 redelivered 将是 false 而不是像正常重新排队一样的 true - Kien Nguyen
3
Kafka可以利用分区的方式在应用程序级别定义部分顺序,从而实现并行化,这对实际任务非常实用。RabbitMQ似乎提供全局排序但没有并行化或者根本没有排序。哪种方案的保证更好? - Andrey.Kozyrev
2
@bazza - 请提出一个新问题,我会尝试回答它 :) - theMayer
2
@theMayer 这要看情况;我见过其他开发人员使用RabbitMQ的一些用途仅仅是作为tcp套接字的替代,没有任何并行消息架构的迹象。RabbitMQ提供的其他功能(如耐久性等)本身就很有吸引力!像这样使用(因此根本不使用消息模式,这几乎是一种悲剧),保留消息顺序非常有用。 - bazza
显示剩余9条评论

9

Kafka保留消息在分区内的顺序,但不是全局顺序。如果您的数据需要全局顺序和分区,这会使事情变得困难。然而,如果您只需要确保同一用户的所有相同事件等被正确排序地放置在同一个分区中,您可以这样做。生产者负责写入的分区,因此如果您能够逻辑上分区您的数据,这可能更可取。


9
我认为这个问题有两个不同的方面,消费顺序和处理顺序。
消息队列可以在一定程度上保证消息按顺序被消费,但是无法保证它们的处理顺序。
主要的区别在于,消息处理中有一些方面是在消费时无法确定的,例如:
- 如前所述,消费者在处理期间可能会失败。在这种情况下,消息的消费顺序是正确的,但是消费者未能正确地处理它,这将使其返回队列。此时,消费顺序是完好无损的,但处理顺序却不是。 - 如果我们认为“处理”意味着消息现在已被丢弃并完全处理完成,则考虑处理时间不是线性的情况,换句话说,处理一个消息比其他消息需要更长时间的情况。例如,如果消息3的处理时间比通常长,那么消息4和5可能会在消息3之前被消费和处理完成。
因此,即使您设法将消息放回队列的前面(顺序消费的违规行为),仍然无法保证它们也会按顺序处理。
如果您想按顺序处理消息:
1. 始终只有1个消费者实例,或者一个主消费者和几个备用消费者。 2. 或者不使用消息队列,并使用同步阻塞方法进行处理。虽然听起来很糟糕,但在许多情况和业务需求中,这是完全有效甚至是关键的。

2
这是准确的信息,但“消费顺序”没有实际意义。消息"处理"才会导致系统状态的改变。由于消息在“消费”之后但在“处理”之前可以被重新排队,所以所有这些只涉及到处理器在完成之前的临时状态 - 希望您不需要关心它。 - theMayer
我还要补充一点,如果你选择第一种选项,并且以被动模式从RMQ接收事件,并且像在NodeJS中使用事件循环,那么你需要使用一个带有1个预取的单一通道,否则你可能会出现多个消息并行处理,这些消息可能以不同的速度被处理。 - Aalex Gabi
解决这个问题的一种方法是在消息中添加订单号,并且消费者需要在数据库中跟踪消息及其顺序。如果消费者A还没有完成实体记录ID 100的消息订单1的处理,则消费者B不应开始处理实体记录ID 100的消息订单2。消费者B应该等待并重试,直到成功为止。 - Vikash

1

有适当的方法可以保证RabbitMQ订阅中消息的顺序。

如果您使用多个消费者,它们将使用共享的ExecutorService处理消息。另请参见ConnectionFactory.setSharedExecutor(...)。您可以设置一个Executors.newSingleThreadExecutor()

如果您使用一个Consumer和一个队列,您可以使用多个bindingKeys(它们可能带有通配符)绑定此队列。消息将按照它们被消息代理接收到的顺序放入队列中。

例如,您有一个单一的发布者发布消息,其中顺序很重要:

try (Connection connection2 = factory.newConnection();
        Channel channel2 = connection.createChannel()) {
    // publish messages alternating to two different topics
    for (int i = 0; i < messageCount; i++) {
        final String routingKey = i % 2 == 0 ? routingEven : routingOdd;
        channel2.basicPublish(exchange, routingKey, null, ("Hello" + i).getBytes(UTF_8));
    }
}

你现在可能希望从同一个队列中按照发布顺序接收来自两个主题的消息:
// declare a queue for the consumer
final String queueName = channel.queueDeclare().getQueue();

// we bind to queue with the two different routingKeys
final String routingEven = "even";
final String routingOdd = "odd";
channel.queueBind(queueName, exchange, routingEven);
channel.queueBind(queueName, exchange, routingOdd);
channel.basicConsume(queueName, true, new DefaultConsumer(channel) { ... });
Consumer现在将按照发布的顺序接收消息,无论您使用了不同的topics
RabbitMQ文档中有一些很好的5分钟教程,可能会有所帮助: https://www.rabbitmq.com/tutorials/tutorial-five-java.html

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接