一.消息确认
1.介绍
RabbitMQ向消费者发送消息后,会把这条消息删除,如果消费者在处理消息时发生异常,那么这条消息会丢失。为了解决这个问题,RabbitMQ提供了消息确认机制(message acknowledgement)。
消费者在订阅队列时可以指定autoAck参数,根据这个参数设置,可以将消息确认机制分为两类:
1)自动确认:autoAck=true,自动把发出去的消息设置为确认,然后从内存或硬盘中删除,不管消费者是否消费成功;
2)手动确认:autoAck=false,消费者显式调用BasicAck命令,回复确认信号后才删除消息。
当autoAck=false时,RabbitMQ中的队列会被分为两部分,一是等待投递给消费者的消息,二是已经投递给消费者,但是没有收到确认信号的消息。
如果RabbitMQ⼀直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则RabbitMQ会安排该消息重新进入队列。

这里的Ready就是等待投递给消费者的消息数量,Unacked就是已经投递给消费者,但是没有收到确认信号的消息数量。
2.手动确认方法
消费者在收到消息后,可以采取下面三种确认方式:
1)肯定确认:Channel.basicAck(long deliveryTag, boolean multiple)
RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了。
参数:deliveryTag消息的唯一标识,multiple是否批量确认
2)否定确认:Channel.basicReject(long deliveryTag, boolean requeue)
拒绝消息。
参数:requeue表示拒绝后,这条消息如何处理。如果requeue参数设置为true,则RabbitMQ会重新将这条消息存入队列,以便可以发送给下⼀个订阅的消费者。如果requeue参数设置为false,则RabbitMQ会把消息从队列中移除,而不会把它发送给新的消费者。
3)否定确认:Channel.basicNack(long deliveryTag, boolean multiple,boolean requeue)
也是拒绝消息,但是这个可以批量拒绝。
multiple=true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息
3.Spring-AMQP策略
Spring-AMQP对消息确认机制提供了三种策略:
1)AcknowledgeMode.NONE
这种模式下,消息⼀旦投递给消费者,不管消费者是否成功处理了消息,RabbitMQ 就会自动确认
消息,从RabbitMQ队列中移除消息。如果消费者处理消息失败,消息可能会丢失。
2)AcknowledgeMode.AUTO(默认)
这种模式下,消费者在消息处理成功时会自动确认消息,但如果处理过程中抛出了异常,则不会确
认消息。
3)AcknowledgeMode.MANUAL
手动确认模式下,消费者必须在成功处理消息后显式调用basicAck方法来确认消息。如果消息未被确认,RabbitMQ 会认为消息尚未被成功处理,并且会在消费者可用时重新投递该消息,这种模式提高了消息处理的可靠性,因为即使消费者处理消息后失败,消息也不会丢失,而是可以被重新处理。
要通过配置信息确认要使用的消息确认机制:
bash
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none
二.持久化
RabbitMQ的持久化分为三个部分:交换器持久化、队列持久化和消息持久化。
1.交换器持久化
交换器的持久化是通过在声明交换机时是将durable参数置为true实现的:
java
ExchangeBuilder.topicExchange(Constant.ACK_EXCHANGE_NAME).durable(true).build()
设置为持久化后,RabbitMQ重启,交换机会自动创建,相当于一直存在。
2.队列持久化
队列的持久化是通过在声明队列时将 durable 参数置为 true实现的,durable默认是true。
创建非持久化队列方法如下:
java
QueueBuilder.nonDurable(Constant.ACK_QUEUE).build()
3.消息持久化
消息实现持久化,需要把消息的投递模式( MessageProperties 中的 deliveryMode )设置为2,也就是 MessageDeliveryMode.PERSISTENT。RabbitMQ默认情况下会将消息视为持久化的,除非队列被声明为非持久化,或者消息在发送时被标记为非持久化。
这里要注意,单单只设置消息持久化没用,还要设置队列持久化,因为消息存在队列中,如果队列没了,消息自然也就没了。
java
//⾮持久化信息
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
//持久化信息
channel.basicPublish("",QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
最后要注意,如果将所有消息都设置为持久化,会严重影响MQ的性能,在选择持久化的时候要做好可靠性和吞吐量之间的权衡。
三.发送方确认
发送方确认保证的是发送方发送消息给RabbitMQ的可靠性,RabbitMQ提供了两种方式来控制消息的可靠性:1)confirm确认模式;2)return退回模式。
这两种模式不是互斥的,可以同时存在,confirm保证的是从发送方传到交换机的可靠性,而return模式保证的是从交换机传到队列的可靠性。

1.confirm确认模式
Producer 在发送消息的时候,对发送端设置⼀个ConfirmCallback的监听,无论消息是否到达Exchange,这个监听都会被执行,如果Exchange成功收到,ACK( Acknowledge character,确认字符)为true,如果没收到消息,ACK就为false。
配置:
java
spring:
rabbitmq:
publisher-confirm-type: correlated
2.return退回模式
消息到达Exchange之后,会根据路由规则匹配,把消息放入Queue中。Exchange到Queue的过程,如果⼀条消息无法被任何队列消费(即没有队列与消息的路由键匹配或队列不存在等),可以选择把消息退回给发送者。消息退回给发送者时,我们可以设置⼀个返回回调方法,对消息进行处理。
配置:
spring:
rabbitmq:
publisher-confirm-type: correlated
四.重试机制
在消息传递过程中,可能会遇到各种问题,如网络故障,服务不可用,资源不足等,这些问题可能导致消息处理失败。为了解决这些问题,RabbitMQ 提供了重试机制,允许消息在处理失败后重新发送。但如果是程序逻辑引起的错误,那么多次重试也是没有用的,可以设置重试次数。
配置:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto #消息接收确认
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时⻓为5秒
max-attempts: 5 # 最⼤重试次数(包括⾃⾝消费的⼀次)
使用重试机制时需要注意:
1)自动确认模式下:程序逻辑异常,多次重试还是失败,消息就会被自动确认,那么消息就丢失了;
2)手动确认模式下:程序逻辑异常,多次重试消息依然处理失败,无法被确认,就⼀直是unacked的状态,导致消息积压。
五.TTL
TTL(Time to Live),即过期时间。RabbitMQ可以对消息设置TTL。当消息到达存活时间之后还没有被消费,那就会被自动清除。
1.设置消息TTL
针对每条消息设置TTL的方法是在发送消息的方法中加入expiration的属性参数,单位为毫秒。
设置消息TTL,消息过期,并不会马上从队列中删除,而是在即将投递到消费者之前进行判断,如果过期了,删掉。因为设置消息TTL的方式,每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。
java
@RequestMapping("/ttl")
public String ttl() {
System.out.println("ttl...");
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "ttl test 10s...", message -> {
message.getMessageProperties().setExpiration("10000"); //单位: 毫秒, 过期时间为10s
return message;
});
return "消息发送成功";
}
2.设置队列TTL
设置队列TTL的方法是在创建队列时,加入 x-message-ttl 参数实现的,单位是毫秒。
设置队列TTL,队列过期,马上从队列中删除。因为设置队列过期时间,队列中已过期的消息肯定在队列头部,RabbitMQ只要定期从队头开始扫描是否有过期的消息即可。
java
//设置ttl
@Bean("ttlQueue")
public Queue ttlQueue(){
//设置队列的ttl为20s
return QueueBuilder.durable(Constants.TTL_QUEUE).ttl(20000).build();
}
六.死信队列
死信(dead message) 简单理解就是因为种种原因,无法被消费的信息。
当消息在⼀个队列中变成死信之后,它能被重新被发送到另⼀个交换器中,这个交换器就是DLX( Dead Letter Exchange ),绑定DLX的队列,就称为死信队列(DeadLetter Queue,简称DLQ)。

消息变成死信一般是以下几个原因:
1)消息被拒绝,并且设置requeue为false
2)消息过期;3)队列达到最大长度。
将正常队列与死信交换机绑定:
java
return QueueBuilder.durable(Constant.NORMAL_QUEUE)
.deadLetterExchange(Constant.DLX_EXCHANGE_NAME)
.deadLetterRoutingKey("dlx").build();
七.延迟队列
延迟队列(Delayed Queue),即消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
注意RabbitMQ本身没有延迟队列,我们是通过TTL+死信队列模式实现延迟队列。
假设⼀个应用中需要将每条消息都设置为10秒的延迟,生产者通过 normal_exchange 这个交换器将发送的消息存储在 normal_queue 这个队列中。消费者订阅的并非是 normal_queue 这个队列,而是 dlx_queue 这个队列。当消息从 normal_queue 这个队列中过期之后被存入 dlx_queue 这个队列中,消费者就恰巧消费到了延迟10秒的这条消息。
但是TTL+死信队列的方式其实是有问题的,如果我们同时将两个消息放入队列,第一个是20s过期,第二个是10s过期。我们的期望结果是10s的先进入死信队列,但是实际上是20s先进入死信队列。因为RabbitMQ只会检查队首的消息是不是过期了。
所以在考虑使用TTL+死信队列实现延迟任务队列的时候,需要确认业务上每个任务的延迟时间是⼀致的,如果遇到不同的任务类型需要不同的延迟的话,需要为每⼀种不同延迟时间的消息建立单独的消息队列。

RabbitMQ提供了一个插件来实现延迟功能:Scheduling Messages with RabbitMQ | RabbitMQ
八.事务
RabbitMQ是基于AMQP协议实现的,该协议实现了事务机制,因此RabbitMQ也支持事务机制.。SpringAMQP也提供了对事务相关的操作。RabbitMQ事务允许开发者确保消息的发送和接收是原子性的,要么全部成功,要么全部失败。
九.消息分发
RabbitMQ队列拥有多个消费者时,队列会把收到的消息分派给不同的消费者。每条消息只会发送给订阅列表里的⼀个消费者。这种方式非常适合扩展,如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可。
默认情况下,RabbitMQ是以轮询的方法进行分发的,而不管消费者是否已经消费并已经确认了消息。这种方式是不太合理的,试想⼀下,如果某些消费者消费速度慢,而某些消费者消费速度快,就可能会导致某些消费者消息积压,某些消费者空闲,进而应用整体的吞吐量下降。
如何处理呢?我们可以使用channel.basicQos(int prefetchCount)方法,来限制当前信道上的消费者所能保持的最大未确认消息的数量。
比如:消费端调用了 channelbasicQos(5),RabbitMQ会为该消费者计数,发送⼀条消息计数+1, 消费⼀条消息计数-1,当达到了设定的上限,RabbitMQ就不会再向它发送消息了,直到消费者确认了某条消息。
常见的应用场景:1)限流;2)非公平分发。