1. 消息确认
1.1 消息确认机制
生产者发送消息之后,到达消费端之后,可能会有以下情况:
- 消息处理成功
- 消息处理异常

RabbitMQ向消费者发送消息之后,就会把这条消息删掉,那么第二种情况,就会造成消息丢失,那么如何确保消费端已经成功接收了,并且正确处理了
为了保证消息从队列可靠到达消费者,RabbitMQ提供了消息确认机制
消费者在订阅队列时,可以指定autoAck参数,根据这个参数设置,消息机制分为以下两种:
- 自动确认:当 auotAck 等于 true时,会自动把发送出去的消息置为确认,然后从内存(或磁盘)删除,而不管消费者是否真正消费了这些消息,自动确认模式适合随遇消息可靠性要求不高的场景
- 手动确认:当 autoAck 等于 false 时,RabbitMQ会等待消费者显式地调用 Basic Ack 命令,回复确认信号后才从内存(或磁盘)中一区消息,这种模式适合对消息可靠性要求比较高的场景
java
String basicConsume(String queue, boolean autoAck, Consumer callback) throws
IOException;
当autoAck参数置为false,对于RabbitMQ服务端而言,队列中的消息分成了两部分:
一是等待投递给消费者的消息
二是已经投递给消费者,但是还没有收到消费者确认信号的消息
如果RabbitMQ一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开了连接,则RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能是原来那个消费者

1.2 手动确认方法
消费者在收到消息之后,可以选择确认,也可以选择直接拒绝或者跳过,RabbitMQ也提供了不同的确认应答方法,消费者客户端可以调用与其对应的channel的相关方法
1)肯定确认
Channel。basicAck(long delivertTag,boolean multiple)
RabbitMQ已经知道该消息发送成功并且被成功消费了,可以将其丢弃了
参数说明:
- deliveryTag:消息的唯一表示,它是一个单调递增的64位的长整型值,是每个通道独立维护的,所以在每个通道上都是唯一的,当消费者确认一条消息时,必须使用对应的通道来确认
- multiple:是否批量确认,在某些情况下,为了减少网络流量,可以对一系列连续的deliveryTag进行确认,值为true则会一次性ack所有小于或等于deliveryTag的消息,值为false则值确认当前指定deliveryTag的消息
2)否定确认
Channel.basicReject(long deliveryTag,boolean requeue)
消费者客户端可以调用方法来告诉RabbitMQ拒绝这个消息
参数说明:
- deliveryTag
- requeue:表示拒接后,这条消息如何处理,如果requeue参数设置为true,会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者,如果设置为false,会把消息从队列中移除
3)否定确认
Channel。basicNack(long deliveryTag,boolean multiple,boolean requeue)
可以批量拒绝消息
multiple参数设置为true则表示拒绝deliveryTag之前所有未被当前消费者确认的消息
1.3 代码示例
spring-AMQP 对消息确认机制提供了三种策略
java
public enum AcknowledgeMode {
NONE,
MANUAL,
AUTO;
}
1)AcknowledgeMode.NONE
消息一旦递给消费者,不管消费者是否成功处理了消息,RabbitMQ都会自动确认消息,从队列中移除消息,如果消费者处理消息失败,消息可能会丢失
2)AcknowledgeMode.AUTO(默认)
消费者在消息处理成功时会自动确认消息,但如果过程中抛出了异常,则不会确认
3)AcknowledgeMode.MANUAL
手动确认模式下,消费者必须在成功处理消息后显式调用 basicAck方法来确认消息,如果消息未被确认,RabbitMQ会认为消息尚未被成功处理,并且会在消费者可用时重新投递该消息,这种模式提高了消息处理的可靠性,因为即使消费者处理消息后失败,消息也不会丢失,而是可以被重新处理


配置是none时,消息会丢失
配置是auto时,控制台会不断输出错误信息

配置为 manual时,要手动签收
java
@Component
public class AckQueueListener {
//指定监听队列的名称
@RabbitListener(queues = Constant.ACK_QUEUE)
public void ListenerQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1. 接收消息
System.out.printf("接收到消息: %s, deliveryTag: %d%n", new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());
//2. 处理业务逻辑
System.out.println("处理业务逻辑");
//⼿动设置⼀个异常, 来测试异常拒绝机制
// int num = 3/0;
//3. ⼿动签收
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
//4. 异常了就拒绝签收
//第三个参数requeue, 是否重新发送, 如果为true, 则会重新发送
// 若为false, 则直接丢弃
channel.basicNack(deliveryTag, true,true);
}
}
}
异常后会不断重试

2. 持久化
RabbitMQ的持久化分为三部分:交换器持久化、队列持久化、消息持久化
2.1 交换机持久化
交换器的持久化是通过声明交换机时将durable参数设置为true实现的,相当于交换机的属性在服务器内部保存,在MQ服务器发送意外关闭后,重启时不需要重新建立交换机,交换机会自动建立,相当于一直存在
如果交换机不设置持久化,那么在重启后,相关的交换机元数据会丢失,对一个长期使用的交换器来说,建议将其值为持久化
ExchangeBuilder.topicExchange(Constant.ACK_EXCHANGE_NAME).durable(true).build()
2.2 队列持久化
队列的持久化是在声明队列时将 durable 参数设置为true
如果队列不设置持久化,在服务器重启后,队列就会被删掉,此时数据也会丢失
队列的持久化能保证队列本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失,要确保消息不会丢失,需要将消息设置为持久化
QueueBuilder.durable(Constant.ACK_QUEUE).build();
进入源码会发现,该方法默认durable是true
java
public static QueueBuilder durable(String name) {
return (new QueueBuilder(name)).setDurable();
}
java
private QueueBuilder setDurable() {
this.durable = true;
return this;
}
如果要设置为非持久化
QueueBuilder.nonDurable(Constant.ACK_QUEUE).build();
2.3 消息持久化
消息实现持久化,需要把消息的投递模式(MessageProperties 中的 deliveryMode)设置为2
也就是 MessageDeliveryMode.PERSISINT
java
public enum MessageDeliveryMode {
NON_PERSISTENT,//⾮持久
PERSISTENT;//持久化
}
设置了队列和消息的持久化,但RabbitMQ服务器重启后,消息依然存在,如果只设置队列持久化,重启后消息会丢失,如果只设置消息持久化,重启后队列会消失,数据也会消失、
java
//⾮持久化信息
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
//持久化信息
channel.basicPublish("",QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
spring中默认是持久化
java
// 创建⼀个Message对象,设置为非持久化
Message messageObject = new Message(message.getBytes(), new
MessageProperties());
messageObject.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSIS
TENT)
rabbitTemplate.convertAndSend(Constant.ACK_EXCHANGE_NAME, "ack",
messageObject);
将交换器、队列、消息都设置了持久化也不能100%保证数据不丢失
1)从消费者角度来看,如果在订阅消费队列时将autoAck设置为true,那么当消费者接收到相关信息之后,还没来得及处理就宕机了,这样也算数据丢失,进行手动确认可解决
2)在持久化的消息正确存入RabbitMQ只有,还需要一段时间才能存入硬盘中,如果在此时间发生了宕机,重启等异常情况,消息保存还没来得及落盘,那么消息将会丢失
如何解决:
1)引入仲裁队列(后续会将)
2)在发送端引入事务机制或者发送方确认机制来保证消息已经正确的发送并存储到RabbitMQ中
3. 发送方确认
3.1 confirm确认模式
Producer 在发送消息的时候,对发送端设置了一个 ConfirmCallback 监听,无论消息是都到达Exchange,这个监听都会执行,如果Exchange 成功收到,ACK确认为true
配置

java
@Bean("confirmRabbitTemplate")
public RabbitTemplate confirmRabbitTemplate(ConnectionFactory
connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.printf("");
if (ack){
System.out.printf("消息接收成功, id:%s \n", correlationData.getId());
}else {
System.out.printf("消息接收失败, id:%s, cause: %s", correlationData.getId(), cause);
}
}
});
return rabbitTemplate;
}
java
@Resource(name = "confirmRabbitTemplate")
private RabbitTemplate confirmRabbitTemplate;
@RequestMapping("/confirm")
public String confirm() throws InterruptedException {
CorrelationData correlationData1 = new CorrelationData("1")
confirmRabbitTemplate.convertAndSend(Constant.CONFIRM_EXCHANGE_NAME,
"confirm", "confirm test...", correlationData1);
return "确认成功";
}
3.2 return模式

java
@Bean("confirmRabbitTemplate")
public RabbitTemplate confirmRabbitTemplate(CachingConnectionFactory connectionFactory{
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
System.out.printf("消息被退回: %s", returned);
}
});
return rabbitTemplate;
}
java
public class ReturnedMessage {
//返回的消息对象,包含了消息体和消息属性
private final Message message;
//由Broker提供的回复码, 表⽰消息⽆法路由的原因. 通常是⼀个数字代码,每个数字代表不同的含义.
private final int replyCode;
//⼀个⽂本字符串, 提供了⽆法路由消息的额外信息或错误描述.
private final StringreplyText;
//消息被发送到的交换机名称
private final String exchange;
//消息的路由键,即发送消息时指定的键
private final String routingKey;
}
4. 重试机制
在消息传递过程中,可能会遇到各种问题,如网络故障,服务不可用,资源不足等,这些问题都可能导致消息处理失败,为了解决这些问题,RabbitMQ提供了重试机制,允许消息在处理失败后重新发送







如果为手动确认,重试次数的限制不会像在自动确认模式下那样直接生效,因为是否重试以及何时重试取决于应用程序的逻辑和消费者的实现
自动确认模式下,RabbitMQ会在消息投递着给消费者后自动确认,如果消费者处理消息时抛出异常,根据配置的重试参数自动将信息重新入队,从而实现重试,重试次数和重试间隔参数可以直接在配置中设置,并且RabbitMQ会负责执行这些重试策略
手动确认模式下,消费者需要显式的对消息进行确认,如果消费者在处理消息时遇到异常,可以选择不确认消息可以重新入队,重试控制权在与程序本身,而不是RabbitMQ内部机制,应用程序可以通过自己的逻辑利用RabbitMQ的改机特性来实现有效的重试策略
5. TTL
RabbitMQ可以对队列和消息设置TTL(过期时间)
当消息到达存活时间之后,还没有被消费,就会自动清除
5.1 设置消息的TTL



5.2 设置队列的TTL


5.3 两者区别
设置队列TTL属性的方法,一旦消息过期,就会从队列中删除
设置消息TTL的方法,即使消息过期,也不会马上从队列中删除,而是在即将投递到消费者之前进行判定
为什么两种方法的处理方式不一样?
因为设置队列过期时间,队列中已经过期的消息在队列头部,RabbitMQ只要定期从队头开始扫描是否有过期的消息即可
而设置消息TTL的方式,每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,所以不如等到此消息即将给消费者时再判断
如果先放入 a消息(30s)b消息(10s),最后b过期的时间也为30s
6. 死信队列
6.1 死信的概念
死信(dead message)简单理解就是因为种种原因,无法被消费的消息,就是死信
有死信,自然就有死信队列,当消息在一个队列中变成死信后,他能被重新发送到另一个交换机中,这个交换器就是DLX(Dead Letter Exchange),绑定DLX的队列,就会称为死信队列(DLQ)

消息变成死信一半是由于以下几种情况:
- 消息被拒绝,且不被允许重新入队
- 消息过期、
- 队列到达对打的长度
6.2 代码示例






7. 延迟队列
7.1 概念
将消息发送出去后,并不想让消费者立即拿到消息,而是等待特定时间后,消费者此案拿到这个消息进行消费
7.2 应用场景
延迟队列的使用场景有很多:
-
智能家居:用户希望通过手机远程遥控家里的智能设备在指定时间进行工作,这时候就可以将用户指令发送到延迟队列中,当指定设定的时间到了再将指令推送到智能设备
-
日常管理:预约会议后,需要在会议开始前事务分钟提醒参会人参会
-
用户注册成功后,7天后发送短信,提高用户活跃度
-
........
RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过TTL+死信队列的方式组合模拟出延迟队列的功能
存在问题:
设置消息过期时出现的问题会影响延迟队列的结果
7.3 延迟队列插件
Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub








8. 事务
RabbitMQ基于AMQP协议实现的,该协议实现了事务机制,因此RabbitMQ也支持事务机制,Spring AMQP也提供了事务相关的操作,RabbitMQ事务允许开发者确保消息的发送和接收是原子性的,要么全部成功,要么全部失败
配置事务管理器(TransRabbitRTemplate)以及 @Transactional




9. 消息分发
9.1 概念
RabbitMQ队列拥有多个消费者,队列会把收到的消息分派给不同的消费者,每条消息只会发送给订阅列表里的一个消费者,这种方式非常适合扩展,如果现在负载加重,只需要创建更多的消费者来消费处理消息即可
默认情况下,RabbitMQ是以轮询的方法进行分发的,而不管消费者是否已经消费并确认,这种方式是不太合理的
可以使用 Channel.basicQos(int prefetchCount)方法来限制当前信道上的消费者所能保持的最大未确认消息的数量

9.2 应用场景
限流
用来实现流控制和负载均衡
非公平分发

可以使用 prefetch=1,告诉RabbitMQ一次只给一个消费者一条消息,在处理并确认一条消息之前,不要向该消费者发送新消息
