文章目录
- [3. Rabbit MQ 高级特性](#3. Rabbit MQ 高级特性)
-
- [3.1 消息确认](#3.1 消息确认)
-
- [3.1.1 自动确认](#3.1.1 自动确认)
- [3.1.2 手动确认](#3.1.2 手动确认)
- [3.1.3 手动确认的三种方式(RabbitMQ)](#3.1.3 手动确认的三种方式(RabbitMQ))
-
- [1. 肯定确认](#1. 肯定确认)
- [2. 否定确认--单个](#2. 否定确认--单个)
- [3. 否定确认--批量](#3. 否定确认--批量)
- [3.1.3 消息确认的三种方式(Spring-AMQP)](#3.1.3 消息确认的三种方式(Spring-AMQP))
- [3.2 持久性(可靠性保证机制)](#3.2 持久性(可靠性保证机制))
-
- [3.2.1 交换机持久化(默认是持久的)](#3.2.1 交换机持久化(默认是持久的))
- [3.2.2 队列持久化(默认是持久化的)](#3.2.2 队列持久化(默认是持久化的))
- [3.2.3 消息持久化](#3.2.3 消息持久化)
- [3.2.4 总结](#3.2.4 总结)
- [3.3 发送方确认(可靠性保证机制)](#3.3 发送方确认(可靠性保证机制))
-
- [3.3.1 confirm确认模式](#3.3.1 confirm确认模式)
- [3.3.2 return退回模式](#3.3.2 return退回模式)
- [3.4 重试机制(在自动确认下才生效)](#3.4 重试机制(在自动确认下才生效))
- [3.5 TTL](#3.5 TTL)
-
- [3.5.1 设置消息的TTL & 设置队列的TTL](#3.5.1 设置消息的TTL & 设置队列的TTL)
- [3.5.2 两者区别](#3.5.2 两者区别)
- [3.6 死信队列](#3.6 死信队列)
-
- [3.6.1 死信的概念](#3.6.1 死信的概念)
- [3.7 延迟队列](#3.7 延迟队列)
-
- [3.7.1 延迟的概念](#3.7.1 延迟的概念)
- [3.7.2 应用场景](#3.7.2 应用场景)
- [3.7.3 TTL + 死信队列实现延迟队列](#3.7.3 TTL + 死信队列实现延迟队列)
- [3.7.4 延迟队列插件](#3.7.4 延迟队列插件)
- [3.8 事务](#3.8 事务)
- [3.9 消息分发](#3.9 消息分发)
-
- [3.9.1 概念](#3.9.1 概念)
- [3.9.2 应用场景](#3.9.2 应用场景)
-
- [3.9.2.1 限流](#3.9.2.1 限流)
- [3.9.2.2 负载均衡](#3.9.2.2 负载均衡)
更多RabbitMQ相关知识: RabbitMQ专栏
3. Rabbit MQ 高级特性
3.1 消息确认
RabbitMQ 向消费者发送消息之后,就会把这条消息删除掉,这样就可能会造成消息丢失。消息确认机制是为了保证消息从队列可靠的到达消费者。
消费者在订阅队列时,可以通过 autoAck 参数进行设置消息确认机制。消息确认机制分为自动确认和手动确认两种。
3.1.1 自动确认
当 autoAck 等于true时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存/磁盘中删除该消息。只要消息到达消费者就会自动确认,不管消费者是否真正地消费到了这些消息。
3.1.2 手动确认
当 autoAck 等于 false 时,RabbitMQ会等待消费者显式地调用Basic.Ack命令,回复确认信号后才会从内存/磁盘中删除该消息。
此时,队列中的消息有两种:
1)等待投递的消息。
2)已经投递,等待消费者确认的消息。
如果 RabbitMQ 一直没有收到消息的确认信号,并且消费该消息的消费者已经断开连接,RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者。

3.1.3 手动确认的三种方式(RabbitMQ)
1. 肯定确认
Channel.basicAck(long deliveryTag, boolean multiple)
deliveryTag: 消息的唯一标识,它是一个单调递增的64位的长整型值。deliveryTag是每个通道(Channel)独立维护的,所以在每个通道上都是唯一的,当消费者确认一条消息时,必须使用对应的通道进行确认。multiple: 是否批量确认。在某些情况下,为了减少网络流量,可以对一系列连续的deliveryTag进行批量确认。值为 true 则会一次性 ack 所有小于或等于指定deliveryTag的消息。值为false,则会确认当前指定deliveryTag的消息。

2. 否定确认--单个
Channel.basicReject(long deliveryTag, boolean requeue)
requeue: 表示拒绝后,这条消息如何让处理。requeue=true 表示RabbitMQ 会重新把这条消息存入队列,以便可以发送给下一个订阅的消费者;requeue=false,RabbitMQ 会把消息从队列中移除,不会把它发送给新的消费者。
3. 否定确认--批量
Channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)
Basic.reject命令:一次只能拒绝一条消息。
Basic.Nack命令:可以批量拒绝消息。
3.1.3 消息确认的三种方式(Spring-AMQP)
Ackmowledge.NONE
对应 RabbitMQ 中的消息自动确认。
在这种模式下,消息一旦被投递给消费者,无论消费者是否正确处理了该消息,RabbitMQ 都会自动确认该消息,并把该消息从队列中移除,有消息丢失的风险。
Ackmowledge.AUTO
只有在消息处理成功之后,才自动确认消息,如果处理过程中抛出了异常,则不会确认消息。
当消费者出现异常的时候,RabbitMQ 会不断的进行重发,如果一直失败,消息无法被确认,也无法nack,就会导致消息积压。
Ackmowledge.MANUAL
对应 RabbitMQ 中的消息手动确认。
手动确认模式下,消费者必须在成功处理消息后,并显式调用 basicAck 方法来确认消息。如果消息未被确认,RabbitMQ 会认为消息未被成功处理,并且会在消费者可用时,重新投递该消息。
这种模式提高了消息处理的可靠性,因为即使消费者处理消息失败,消息也不会丢失,而是可以被重新处理。
3.2 持久性(可靠性保证机制)
持久性可以保证 RabbitMQ 异常退出或者由于某种原因崩溃时,保证消息不丢失。
3.2.1 交换机持久化(默认是持久的)
交换机的持久化相当于将交换机的属性在服务器内部保存,当MQ的服务器发生意外或者关闭之后,重启 RabbitMQ 时不需要去重新创建交换机,交换机会自动创建,相当于一直存在(嚯,不死真身🤓)。
相反,如果交换机不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换机的原数据会丢失,对一个长期使用的交换机来说,是建议将其置为持久化的。
java
ExchangeBuilder.topicExchange(Constant.ACK_EXCHANGE_NAME).durable(true).build()
3.2.2 队列持久化(默认是持久化的)
如果不设置队列的持久化,那么在 RabbitMQ 服务重启之后,该队列就会被删除掉,此时队列中的数据也会丢失。
设置了队列的持久化能够保证队列本身的原数据不会因为异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。
要想保证消息不丢失,还需要将消息设置为持久化。
java
QueueBuilder.durable(Constant.ACK_QUEUE).build();
3.2.3 消息持久化
由于消息是存储在队列中的:
-
队列持久化 + 消息持久化 = 消息不丢失。
-
队列持久化 + 消息不持久化 = 消息丢失。
-
队列不持久化 + 消息持久化 = 消息丢失。
3.2.4 总结
如果将所有的消息都设置为持久化,会严重的影响到 RabbitMQ 的性能。因为写入磁盘的速度要比写入内存的速度慢的多。所以出于对性能的考虑,对于可靠性要求不是那么高的消息,可以不采取持久化处理,来提高整体的吞吐量。在选择是否要将消息持久化的时候,需要在可靠性和吞吐量之间做一个权衡。
问:将交换机、队列、消息都设置成持久化之后就能百分百保证数据不丢失吗?
答:并不是。
-
从消费者来说,如果在订阅消息队列时将 autoAck 参数设置为 true,如果消费者接收到相关消息之后,还没来得及处理该消息就宕机了,这样也算是数据丢失。想要解决这种情况就需要设置消息手动确认。
-
在持久化的消息正确存入 RabbitMQ 之后,还需要有一段时间才能存入磁盘中。RabbitMQ 并不会为每条消息都进行同步存盘处理,可能仅仅保存到操作系统缓存中而不是物理磁盘中。如果在这段时间内 RabbitMQ 服务节点发生了宕机、重启等异常情况,消息保存还没来得及落盘,那么这些消息也将会丢失。
3.3 发送方确认(可靠性保证机制)
发送方确认是保证消息正确到达服务器的。
Rabbit MQ提供了两种方式来控制消息的可靠性投递:
-
confirm 确认模式。
-
return 退回模式。
注意: 这两种方式不互斥,可以单独使用,也可以结合使用。

3.3.1 confirm确认模式
Producer 在发送消息的时候,对发送端设置一个 ConfirmCallback 的监听,无论消息是否到达交换机,这个监听都会被执行,如果交换机成功收到,ACK确认为true;如果交换机没有收到消息,ACK就为false。
问:RabbitTemplate.ConfirmCallback 和 ConfirmListener 的区别?
答: 在 RabbitMQ 中,ConfirmListener 和 ConfirmCallback 都是用来处理消息确认的机制,但它们属于不同的客户端库,并且使用的场景和方式有所不同。
1)ConfirmListener 是 RabbitMQ Java Client 库中的接口。这个库是 RabbitMQ 官方提供的一个直接与 RabbitMQ 服务器交互的客户端库。ConfirmListener 接口提供了两个方法:handleAck 和 handleNack,用于处理消息确认和否认确认的事件。
2 )ConfirmCallback 是 Spring AMQP 框架中的一个接口。专门为Spring环境设计,用于简化与 RabbitMQ 交互的过程。它只包含一个confirm方法,用于处理消息确认的回调.
在 Spring Boot 应用中, 通常会使⽤ ConfirmCallback, 因为它与 Spring 框架的其他部分更加整合, 可以利⽤ Spring 的配置和依赖注⼊功能. ⽽在使⽤ RabbitMQ Java Client 库时, 则可能会直接实现 ConfirmListener 接⼝, 更直接的与 RabbitMQ 的 Channel 交互 。
3.3.2 return退回模式
消息到达交换机之后,会根据路由规则匹配,把消息放入 Queue 中。消息从交换机到队列的过程中,如果一条消息无法被任何队列消费,可以选择把消息退回给发送者。
消息退回给发送者时,可以设置一个回调方法,对被退回的消息进行处理。
3.4 重试机制(在自动确认下才生效)
在消息传递过程中,可能会遇到各种问题,这些问题可能会导致消息处理失败,为了解决这些问题, RabbitMQ 提供了重试机制,允许消息在处理失败后重新发送。
1. 自动确认模式下: 程序逻辑异常,多次重试还是失败,消息就会被自动确认,那么消息就丢失了。
2. 手动确认模式下: 程序逻辑异常,多次重试消息依然处理失败,无法被确认,就一直是unacked的状态,后面的消息无法被正常消费,导致消息积压。
3.5 TTL
TTL:过期时间。
当消息到达存活时间之后,如果还没有被消费,就会被自动清除掉。
3.5.1 设置消息的TTL & 设置队列的TTL
两种方法设置消息的TTL:
-
设置队列的TTL。队列中所有消息都有相同的过期时间。
-
对消息本身进行单独的设置,每条消息的TTL可以不同。
如果两种方法一起使用,消息的TTL以两者之间较小的那个TTL为准。
3.5.2 两者区别
设置队列的TTL,一旦消息过期,就会从队列中删除。
设置消息的TTL,即使消息过期,也不会马上从队列中删除,而是在即将投递到消费者之前进行判定的。
问:为什么这两种方法处理的方式不一样?
因为设置队列过期时间,队列中已过期的消息肯定在队列头部,RabbitMQ 只要定期从对头开始扫描是否有过期的消息即可。
而设置消息的TTL的方式,每条消息的TTL不同,如果要删除所有过期消息需要扫描整个队列,资源的开销有点大。所以,不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。
3.6 死信队列
3.6.1 死信的概念
死信:由于种种原因,无法被消费的消息,就是死信。
当消息在一个队列中变成死信之后,它能重新被发送到另一个交换机中,这个交换器就是DLX,绑定DLX的队列,就称为死信队列。
消息变成死信的几种情况:
-
消息被拒绝并且不能重新进入队列。
-
消息过期。
-
队列达到最大长度。

3.7 延迟队列
3.7.1 延迟的概念
延迟队列:在消息被发送之后,并不想让消费者立刻拿到消息。而是等待特定的时间之后,消费者才能拿到这个消息进行消费。
3.7.2 应用场景
- 智能家居:用户恶意通过手机远程遥控家里的智能设备在指定的时间进行工作,这个时候就可以将用户的指令发送到延迟队列,当指令预设的时间到了再将指令推送到智能设备。
- 日常管理:预定会议,需要在会议开始的前15分钟提醒人参加会议。
- ...
RabbitMQ 本身并不直接支持延迟队列的功能,但是可以通过 TTL+死信队列的方式组合模拟出延迟队列的功能。

假设一个应用中需要将每条消息都设置为10s的延迟,生产者通过 normal_exchange 这个交换机将发送的消息存储在normal_queque这个队列中。消费者订阅的并非是normal_queque这个队列,而是dlx_queque这个队列,当消息从normal_queque这个队列中过期之后被存入dlx_queue这个队列中,消费者就恰巧消费到了延迟10s的这条消息。
3.7.3 TTL + 死信队列实现延迟队列
假设,我们现在发送了两条消息,第一条的 TTL 为30s,第二条的 TTL 为10s。
这时,我们就会发现一个问题,第二条消息并不是在第10s进入死信队列的,而是延后了20s,和第一条消息一起进入死信队列的。
由于队列是先进先出,RabbitMQ 只会检查队首的消息是否过期。如果过期则会将消息丢到死信队列中。所以就导致,第二条消息在过期的时候 RabbitMQ 并不知道,直到第一条消息被 RabbitMQ 检测到过期了。才会检测第二条消息,所以就导致第二条消息进入死信队列的时间延后了20s.
所以在考虑使用 TTL 和死信队列实现延迟队列的时候,需要确定业务上每个任务的延迟时间是一致的,如果遇到不同任务类型,需要不同延迟的话,需要为每一种不同延迟时间的消息,建立单独的消息队列。
3.7.4 延迟队列插件
延迟队列插件是 RabbitMQ 提供的,实现了延迟的功能。
使用延迟插件实现的延迟队列,解决了上面同一个队列中消息的延迟时间不同的问题。
3.8 事务
RabbitMQ 是基于 AMQP 协议实现的,该协议实现了事务机制,因此 RabbitMQ 也支持事务机制。
SpringAMQP 也提供了事务的相关操作,RabbitMQ 事务允许开发者确保消息的发送和接收时原子性的,要么全部成功,要么全部失败。比如:一次发送了两条消息,这两条消息要么全部发送成功,要么全部发送失败。
3.9 消息分发
3.9.1 概念
RabbitMQ 队列拥有多个消费者时,队列会把收到的消息分派给不同的消费者,每条消息只会发送给订阅者列表中的一个消费者。
默认情况下,RabbitMQ 是以轮询的方式进行分发的,但是每个消费者的消费能力不同,如果某些消费者的消费速度很慢,而某些消费者消费速度快,就会导致消息积压。
基于这种情况,我们就可以使用 channel.basicQos 方法来限制当前信道上的消费者所能保持的最大未确认消息的数量。比如:消费端调用 channelbasicQos(5),RabbitMQ 会为该消费者计数,发送一条消息计数 +1 ,消费一条消息计数 -1,当达到设定的上限,RabbitMQ 就不会再给这个消费者发送消息,直到消费者确认了某条消息,消息计数 -1。
3.9.2 应用场景
消息分发 常见场景:
-
限流
-
非公平分发
3.9.2.1 限流
比如,正常情况下,订单系统每秒最多处理5000个请求,日常是可以满足需求的。
但是如果是在秒杀的时间点,请求量瞬间增多,如果这些请求全部通过 MQ 发送到订单系统,无疑会把订单系统压垮。

使用 RabbitMQ 的限流机制,就可以控制消费端一次只拉取N个请求。
限流机制:
-
消息应答方式设置为手动应答。
-
设置 prefetchCount 参数
3.9.2.2 负载均衡
根据消费者处理任务的快慢,进行分派任务。

prefetch=1 表示:RabbitMQ 一次只给消费者一条消息,该消费者在处理并确认一条消息之前,不要向该消费者发送新的消息。