
RabbitMQ的高级特性还包括我的上篇博客
【RabbitMQ】-----详解RabbitMQ高级特性之消息确认机制-CSDN博客
目录
[return 模式](#return 模式)
[TTL+死信队列 实现延迟队列](#TTL+死信队列 实现延迟队列)
RabbitMQ高级特性之持久性
持久性
RabbitMQ的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。
交换机持久化
交换器的持久化是通过在声明交换机时是将durable参数置为true实现的.相当于将交换机的属性在服务器内部保存,当MQ的服务器发⽣意外或关闭之后,重启 RabbitMQ 时不需要重新去建⽴交换机, 交换机会⾃动建⽴,相当于⼀直存在.
如果交换器不设置持久化, 那么在 RabbitMQ 服务重启之后, 相关的交换机元数据会丢失, 对⼀个⻓期使⽤的交换器来说,建议将其置为持久化的.

重启RabbitMQ服务前:

重启RabbitMQ服务后:

由此我们可以看到,重启RabbitMQ服务之后,重启服务之前声明的持久化交换机依旧存在
交换机设置了非持久化之后,当重启RabbitMQ服务之后,会发现之前声明的非持久化交换机没有了,因为消息依托于队列,而队列依托于交换机,所以当交换机非持久化之后,无论消息和队列是否持久化,当重启RabbitMQ之后,消息,队列和交换机都不复存在了。
队列持久化+消息持久化
因为消息和队列二者之间的关系比较强,所以下面将队列的持久化和非持久化与消息的持久化和非持久化一起来讲解。
队列的持久化是通过在声明队列时将 durable 参数置为 true实现的.
如果队列不设置持久化, 那么在RabbitMQ服务重启之后,该队列就会被删掉, 此时数据也会丢失. (队列没有了, 消息也⽆处可存了)
队列的持久化能保证该队列本⾝的元数据不会因异常情况⽽丢失, 但是并不能保证内部所存储的消息不会丢失. 要确保消息不会丢失, 需要将消息设置为持久化。
消息实现持久化, 需要把消息的投递模式( MessageProperties 中的 deliveryMode )设置为2,也就是 MessageDeliveryMode.PERSISTENT。
设置了队列和消息的持久化, 当 RabbitMQ 服务重启之后, 消息依旧存在. 如果只设置队列持久化, 重启之后消息会丢失. 如果只设置消息的持久化, 重启之后队列消失, 继⽽消息也丢失. 所以单单设置消息持久化⽽不设置队列的持久化显得毫⽆意义
队列持久+消息持久


生产消息

重启RabbitMQ服务后:

此时我们可以看到,当重启RabbitMQ服务后,重启RabbitMQ服务之前声明的持久化队列和持久化消息都还是存在的。
队列持久+消息非持久
重启RabbitMQ服务后:

队列非持久+消息持久
重启RabbitMQ服务后:

队列非持久+消息非持久
重启RabbitMQ服务后:

所以只有当交换机+队列+消息都是持久化的时候才具有持久性
RabbitMQ高级特性之发送方确认机制
发送方确认
在使⽤ RabbitMQ的时候, 可以通过消息持久化来解决因为服务器的异常崩溃⽽导致的消息丢失, 但是还有⼀个问题, 当消息的⽣产者将消息发送出去之后, 消息到底有没有正确地到达服务器呢? 如果在消息到达服务器之前已经丢失(⽐如RabbitMQ重启, 那么RabbitMQ重启期间⽣产者消息投递失败), 持久化操作也解决不了这个问题,因为消息根本没有到达服务器,何谈持久化?
RabbitMQ为我们提供了两种解决⽅案:
a. 通过事务机制实现
b. 通过发送⽅确认(publisher confirm) 机制实现
事务机制⽐较消耗性能, 在实际⼯作中使⽤也不多, 下面主要介绍confirm机制来实现发送⽅的确认.
RabbitMQ为我们提供了两个⽅式来控制消息的可靠性投递:
1. confirm确认模式
2. return退回模式
添加配置
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://study:study@47.98.109.138:5672/extension
publisher-confirm-type: correlated #消息发送确认
常量类
public class Constants {
//发送方确认
public static final String CONFIRM_QUEUE = "confirm.queue";
public static final String CONFIRM_EXCHANGE = "confirm.exchange";
}
声明队列和交换机并绑定二者关系
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class RabbitMQConfig {
//发送方确认
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(Constants.CONFIRM_QUEUE).build();
}
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(Constants.CONFIRM_EXCHANGE).build();
}
@Bean("confirmBinding")
public Binding confirmBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
}
confirm确认模式
Producer 在发送消息的时候, 对发送端设置⼀个ConfirmCallback的监听, ⽆论消息是否到达
Exchange, 这个监听都会被执⾏, 如果Exchange成功收到, ACK( Acknowledge character , 确认字符)为true, 如果没收到消息, ACK就为false。
RabbitTemplate.ConfirmCallback 和 ConfirmListener 区别
在RabbitMQ中, ConfirmListener和ConfirmCallback都是⽤来处理消息确认的机制, 但它们属于不同的客⼾端库, 并且使⽤的场景和⽅式有所不同.
ConfirmListener 是 RabbitMQ Java Client 库中的接⼝. 这个库是 RabbitMQ 官⽅提供的⼀个直接与RabbitMQ服务器交互的客⼾端库. ConfirmListener 接⼝提供了两个⽅法: handleAck 和handleNack, ⽤于处理消息确认和否定确认的事件.
ConfirmCallback 是 Spring AMQP 框架中的⼀个接⼝. 专⻔为Spring环境设计. ⽤于简化与
RabbitMQ交互的过程. 它只包含⼀个 confirm ⽅法,⽤于处理消息确认的回调.
在 Spring Boot 应⽤中, 通常会使⽤ ConfirmCallback, 因为它与 Spring 框架的其他部分更加整合, 可以利⽤ Spring 的配置和依赖注⼊功能. ⽽在使⽤ RabbitMQ Java Client 库时, 则可能会直接实现ConfirmListener 接⼝, 更直接的与RabbitMQ的Channel交互
编写生产消息代码
java
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import rabbitextensionsdemo.constant.Constants;
import java.util.Date;
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate confirmRabbitTemplate;
@RequestMapping("/confirm")
public String confirm() {
confirmRabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("执行了confirm方法");
if (ack){
System.out.printf("接收到消息, 消息ID: %s \n", correlationData==null? null: correlationData.getId());
}else {
System.out.printf("未接收到消息, 消息ID: %s, cause: %s \n", correlationData==null? null: correlationData.getId(), cause);
//相应的业务处理
}
}
});
CorrelationData correlationData = new CorrelationData("1");
confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm", "confirm test...", correlationData);
return "消息发送成功";
}
}
javapublic interface ConfirmCallback { /** * 确认回调 * @param correlationData: 发送消息时的附加信息 , 通常⽤于在确认回调中识别特定的消 息 * @param ack: 交换机是否收到消息 , 收到为 true, 未收到为 false * @param cause: 当消息确认失败时 , 这个字符串参数将提供失败的原因 . 这个原因可以⽤于调 试和错误处理 . * 成功时 , cause 为 null */ void confirm ( @Nullable CorrelationData correlationData, boolean ack, @Nullable String cause); }
生产消息1
第一次生产消息

第二次生产消息

此时我们看到,第一次生产消息时能够正常生产消息,但是当我们第二次生产消息时却抛异常了,异常信息为:java.lang.IllegalStateException: Only one ConfirmCallback is supported by each RabbitTemplate
解决方法
是为什么呢?从异常信息中我们可以看到,ConfirmCallback只能被设置一次,但是从我们的代码中可以看到,我们每次生产消息时都会设置一次ConfirmCallback,显然这就是问题所在。
下面我们把刚刚的ConfirmCallback提取出来,重新设置RabbitTemplate。
RabbitTemplateConfig
java
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitTemplateConfig {
@Bean
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.println("执行了confirm方法");
if (ack){
System.out.printf("接收到消息, 消息ID: %s \n", correlationData==null? null: correlationData.getId());
}else {
System.out.printf("未接收到消息, 消息ID: %s, cause: %s \n", correlationData==null? null: correlationData.getId(), cause);
//相应的业务处理
}
}
});
return rabbitTemplate;
}
}
ProducerController
java
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import rabbitextensionsdemo.constant.Constants;
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitTemplate confirmRabbitTemplate;
@RequestMapping("/pres")
public String pres() {
Message message = new Message("Presistent test...".getBytes(), new MessageProperties());
//消息持久化
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
System.out.println(message);
rabbitTemplate.convertAndSend(Constants.PRES_EXCHANGE, "pres", message);
return "消息发送成功";
}
@RequestMapping("/confirm")
public String confirm() {
CorrelationData correlationData = new CorrelationData("1");
confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm", "confirm test...", correlationData);
return "消息发送成功";
}
}

此时我们可以看到,我们解决了前面多次生产消息导致的ConfirmCallback被设置多次的问题,但是我们此时的代码就真的没有问题了吗?


当我们生产其它消息时,发现我们并没有给这个生产消息的方法设置ConfirmCallback啊,但是为什么在控制台上看到执行了我们设置的ConfrimCallback,这是为什么呢?

是因为我们在前面设置了RabbitTemplate,而且使用了@Autowired注解注入了RabbitTemplate,虽然我们注入了两个,一个是rabbitTemplate,一个是confirmRabbitTemplate,但是这两个都是同一个RabbitTemplate。
解决方法
解决办法:我们在RabbitTemplateConfig中设置两个RabbitTemplate.
java
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
return rabbitTemplate;
}
@Bean
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.println("执行了confirm方法");
if (ack){
System.out.printf("接收到消息, 消息ID: %s \n", correlationData==null? null: correlationData.getId());
}else {
System.out.printf("未接收到消息, 消息ID: %s, cause: %s \n", correlationData==null? null: correlationData.getId(), cause);
//相应的业务处理
}
}
});
return rabbitTemplate;
}
}
与此同时,我们修改注入方式:

此时,当再次使用/producer/pres来生产消息时,就没问题了。
下面我们修改一下生产消息时给消息设置的路由规则:
java
@RequestMapping("/confirm")
public String confirm() {
CorrelationData correlationData = new CorrelationData("1");
confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm111", "confirm test...", correlationData);
return "消息发送成功";
}
生产消息


我们知道,上面生产消息时给消息设置的路由规则并不存在,按道理说,应该会打印"未收到消息"而非"收到消息",原因是因为,上面的confirm确认模式是用来确定生产消息是否到达了交换机,而上面的路由规则是针对消息从交换机到队列的,解决上面的路由问题使用到另一种确认模式。
return 模式
消息到达Exchange之后, 会根据路由规则匹配, 把消息放⼊Queue中. Exchange到Queue的过程, 如果⼀条消息⽆法被任何队列消费(即没有队列与消息的路由键匹配或队列不存在等), 可以选择把消息退回给发送者. 消息退回给发送者时, 我们可以设置⼀个返回回调⽅法, 对消息进⾏处理。
修改RabbitTemplateConfig,设置消息退回的回调方法
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
return rabbitTemplate;
}
@Bean
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.println("执行了confirm方法");
if (ack){
System.out.printf("接收到消息, 消息ID: %s \n", correlationData==null? null: correlationData.getId());
}else {
System.out.printf("未接收到消息, 消息ID: %s, cause: %s \n", correlationData==null? null: correlationData.getId(), cause);
//相应的业务处理
}
}
});
//消息被退回时, 回调方法
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
System.out.println("消息退回:"+returned);
}
});
return rabbitTemplate;
}
}
使⽤RabbitTemplate的setMandatory⽅法设置消息的mandatory属性为true(默认为false). 这个属性的作⽤是告诉RabbitMQ, 如果⼀条消息⽆法被任何队列消费, RabbitMQ应该将消息返回给发送者, 此时 ReturnCallback 就会被触发。
回调函数中有⼀个参数: ReturnedMessage, 包含以下属性:
java
public class ReturnedMessage {
//返回的消息对象,包含了消息体和消息属性
private final Message message;
//由 Broker 提供的回复码 , 表⽰消息⽆法路由的原因 . 通常是⼀个数字代码,每个数字代表不同 的含义 .
private final int replyCode;
//⼀个⽂本字符串 , 提供了⽆法路由消息的额外信息或错误描述 .
private final String replyText;
//消息被发送到的交换机名称
private final String exchange;
//消息的路由键,即发送消息时指定的键
private final String routingKey;
}
编写生产消息代码(路由正确)
java
@RequestMapping("/returns")
public String returns() {
CorrelationData correlationData = new CorrelationData("5");
confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm", "returns test...", correlationData);
return "消息发送成功";
}


编写生产消息代码(路由错误)
java
@RequestMapping("/returns")
public String returns() {
CorrelationData correlationData = new CorrelationData("5");
confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm111", "returns test...", correlationData);
return "消息发送成功";
}


此时我们可以看到,队列中依旧是只有1条消息,而且代码执行了消息退回,而且消息退回时打印了消息信息,显然我们可以看到,消息的路由规则是错误的,不会入队列。
面试题
如何保证RabbitMQ消息的可靠传输?

从这个图中, 可以看出, 消息可能丢失的场景以及解决⽅案:
- ⽣产者将消息发送到 RabbitMQ失败
a. 可能原因: ⽹络问题等
b. 解决办法: [发送⽅确认-confirm确认模式]
- 消息在交换机中⽆法路由到指定队列:
a. 可能原因: 代码或者配置层⾯错误, 导致消息路由失败
b. 解决办法: [发送⽅确认-return模式]
- 消息队列⾃⾝数据丢失
a. 可能原因: 消息到达RabbitMQ之后, RabbitMQ Server 宕机导致消息丢失.
b. 解决办法: [持久性]. 开启 RabbitMQ持久化, 就是消息写⼊之后会持久化到磁盘, 如果RabbitMQ 挂了, 恢复之后会⾃动读取之前存储的数据. (极端情况下, RabbitMQ还未持久化就挂了, 可能导致少量数据丢失, 这个概率极低, 也可以通过集群的⽅式提⾼可靠性)
- 消费者异常, 导致消息丢失
a. 可能原因: 消息到达消费者, 还没来得及消费, 消费者宕机. 消费者逻辑有问题.
b. 解决办法: [消息确认]. RabbitMQ 提供了 消费者应答机制 来使 RabbitMQ 能够感知到消费者是否消费成功消息. 默认情况下消费者应答机制是⾃动应答的, 可以开启⼿动确认, 当消费者确认消费成功后才会删除消息, 从⽽避免消息丢失. 除此之外, 也可以配置重试机制, 当消息消费异常时, 通过消息重试确保消息的可靠性。
RabbitMQ高级特性之重试机制
在消息传递过程中, 可能会遇到各种问题, 如⽹络故障, 服务不可⽤, 资源不⾜等, 这些问题可能导致消息处理失败. 为了解决这些问题, RabbitMQ 提供了重试机制, 允许消息在处理失败后重新发送.但如果是程序逻辑引起的错误, 那么多次重试也是没有⽤的, 可以设置重试次数.
添加配置
配置类
java
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://study:study@47.98.109.138:5672/extension
listener:
simple:
acknowledge-mode: auto #消息接收确认
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时长为5秒
max-attempts: 5 # 最大重试次数
常量类
java
public class Constants {
//重试机制
public static final String RETRY_QUEUE = "retry.queue";
public static final String RETRY_EXCHANGE = "retry.exchange";
}
声明队列和交换机并绑定二者关系
java
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class RabbitMQConfig {
//重试机制
@Bean("retryQueue")
public Queue retryQueue(){
return QueueBuilder.durable(Constants.RETRY_QUEUE).build();
}
@Bean("retryExchange")
public DirectExchange retryExchange(){
return ExchangeBuilder.directExchange(Constants.RETRY_EXCHANGE).build();
}
@Bean("retryBinding")
public Binding retryBinding(@Qualifier("retryQueue") Queue queue, @Qualifier("retryExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("retry").noargs();
}
}
编写生产消息代码
java
@RequestMapping("/retry")
public String retry() {
System.out.println("retry...");
rabbitTemplate.convertAndSend(Constants.RETRY_EXCHANGE, "retry", "retry test...");
return "消息发送成功";
}
编写消费消息代码1(自动确认)
java
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
import java.io.UnsupportedEncodingException;
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void handlerMessage(Message message) throws UnsupportedEncodingException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.printf("["+ Constants.RETRY_QUEUE+"]接收到消息: %s, deliveryTag: %s \n", new String(message.getBody(), "UTF-8"), deliveryTag);
int num = 3/0;
System.out.println("业务处理完成");
}
}

此时我们可以看到,消费方发生了异常,接着进行了5次重试,然后就抛异常了。
编写消费消息代码2(自动确认)
java
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void handlerMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.printf("["+Constants.RETRY_QUEUE+"]接收到消息: %s, deliveryTag: %s \n", new String(message.getBody(), "UTF-8"), deliveryTag);
try {
int num = 3/0;
System.out.println("业务处理完成");
}catch (Exception e){
System.out.println("业务处理失败");
}
}
}
消费消息

此时我们发现,虽然配置了重试次数为5次,但是在处理异常后,并没有进行重试。
编写消费消息代码3(手动确认)
java
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://study:study@47.98.109.138:5672/extension
listener:
simple:
acknowledge-mode: manual #消息接收确认
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时长为5秒
max-attempts: 5 # 最大重试次数
java
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void handlerMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.printf("["+Constants.RETRY_QUEUE+"]接收到消息: %s, deliveryTag: %s \n", new String(message.getBody(), "UTF-8"), deliveryTag);
try {
int num = 3/0;
System.out.println("业务处理完成");
channel.basicAck(deliveryTag, false);
}catch (Exception e){
System.out.println("业务处理失败");
channel.basicNack(deliveryTag, false, true);
}
}
}

因为抛出了异常,设置了手动确认,而且设置了拒绝确认时重新入队,所以会不停地在控制台打印,而且deleveryTag的值不断增加,是因为消息不断重新出队和入队。
可以看到, ⼿动确认模式时, 重试次数的限制不会像在⾃动确认模式下那样直接⽣效, 因为是否重试以及何时重试更多地取决于应⽤程序的逻辑和消费者的实现.
- ⾃动确认模式下, RabbitMQ 会在消息被投递给消费者后⾃动确认消息. 如果消费者处理消息时抛出异常, RabbitMQ 根据配置的重试参数⾃动将消息重新⼊队, 从⽽实现重试. 重试次数和重试间隔等参数可以直接在RabbitMQ的配置中设定,并且RabbitMQ会负责执⾏这些重试策略.
- ⼿动确认模式下, 消费者需要显式地对消息进⾏确认. 如果消费者在处理消息时遇到异常, 可以选择不确认消息使消息可以重新⼊队. 重试的控制权在于应⽤程序本⾝, ⽽不是RabbitMQ的内部机制. 应⽤程序可以通过⾃⼰的逻辑和利⽤RabbitMQ的⾼级特性来实现有效的重试策略。
使⽤重试机制时需要注意:
- ⾃动确认模式下: 程序逻辑异常, 多次重试还是失败, 消息就会被⾃动确认, 那么消息就丢失
了
- ⼿动确认模式下: 程序逻辑异常, 多次重试消息依然处理失败, ⽆法被确认, 就⼀直是
unacked的状态, 导致消息积压
RabbitMQ高级特性之TTL
TTL
TTL(Time to Live, 过期时间), 即过期时间. RabbitMQ可以对消息和队列设置TTL.
当消息到达存活时间之后, 还没有被消费, 就会被⾃动清除。
咱们在⽹上购物, 经常会遇到⼀个场景, 当下单超过24⼩时还未付款, 订单会被⾃动取消
还有类似的, 申请退款之后, 超过7天未被处理, 则⾃动退款。
添加配置
spring: application: name: rabbit-extensions-demo rabbitmq: addresses: amqp://study:study@47.98.109.138:5672/extension
常量类
java
public class Constants {
//ttl
public static final String TTL_QUEUE = "ttl.queue";
public static final String TTL_QUEUE2 = "ttl2.queue";
public static final String TTL_EXCHANGE = "ttl.exchange";
}
消息的TTL
声明队列和交换机并绑定二者关系
java
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class RabbitMQConfig {
@Bean("ttlQueue")
public Queue ttlQueue(){
return QueueBuilder.durable(Constants.TTL_QUEUE).build();
}
@Bean("ttlExchange")
public DirectExchange ttlExchange(){
return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();
}
@Bean("ttlBinding")
public Binding ttlBinding(@Qualifier("ttlQueue") Queue queue, @Qualifier("ttlExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
}
}
编写生产消息代码
java
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import rabbitextensionsdemo.constant.Constants;
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Resource(name = "rabbitTemplate")
private RabbitTemplate rabbitTemplate;
@RequestMapping("/ttl")
public String ttl() {
System.out.println("ttl...");
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "ttl test 30s...", message -> {
message.getMessageProperties().setExpiration("30000"); //单位: 毫秒, 过期时间为30s
return message;
});
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "ttl test 10s...", message -> {
message.getMessageProperties().setExpiration("10000"); //单位: 毫秒, 过期时间为10s
return message;
});
return "消息发送成功";
}
}
生产消息



我们可以看到,生产的两条消息的确消失了,但是耗时30秒,这是为什么呢?
原因是因为设置消息的TTL,哪怕消息过期了,也不会立即删除,而是在将消息投递给消费者之前进行判定
队列的TTL
声明队列和交换机并绑定二者关系
java
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class RabbitMQConfig {
@Bean("ttlQueue2")
public Queue ttlQueue2(){
return QueueBuilder.durable(Constants.TTL_QUEUE2).ttl(20000).build(); //设置队列的ttl为20s
}
@Bean("ttlExchange")
public DirectExchange ttlExchange(){
return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();
}
@Bean("ttlBinding2")
public Binding ttlBinding2(@Qualifier("ttlQueue2") Queue queue, @Qualifier("ttlExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
}
}
编写生产消息代码
java
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import rabbitextensionsdemo.constant.Constants;
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Resource(name = "rabbitTemplate")
private RabbitTemplate rabbitTemplate;
@RequestMapping("/ttl")
public String ttl() {
System.out.println("ttl...");
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "ttl test 30s...", message -> {
message.getMessageProperties().setExpiration("30000"); //单位: 毫秒, 过期时间为30s
return message;
});
return "消息发送成功";
}
@RequestMapping("/ttl2")
public String ttl2() {
System.out.println("ttl2...");
//发送普通消息
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE, "ttl", "ttl test...");
return "消息发送成功";
}
}
生产消息(消息无TTL)


我们可以看到,虽然没有给消息设置TTL,但是给ttl2.queue队列设置了20秒的TTL,20秒过后,ttl2.queue队列中的消息消失了。
生产消息(消息有TTL)


此时我们可以看到,20秒之后,消息消失了,我们给队列设置的TTL为20秒,给消息设置的TTL为30秒,最终消息的TTL 为 min(消息的TTL,队列的TTL)
消息的TTL和队列的TTL
设置队列TTL属性的⽅法, ⼀旦消息过期, 就会从队列中删除
设置消息TTL的⽅法, 即使消息过期, 也不会⻢上从队列中删除, ⽽是在即将投递到消费者之前进⾏判定的.为什么这两种⽅法处理的⽅式不⼀样?
因为设置队列过期时间, 队列中已过期的消息肯定在队列头部, RabbitMQ只要定期从队头开始扫描是否有过期的消息即可.
⽽设置消息TTL的⽅式, 每条消息的过期时间不同, 如果要删除所有过期消息需要扫描整个队列, 所以不如等到此消息即将被消费时再判定是否过期, 如果过期再进⾏删除即可.
RabbitMQ高级特性之死信队列
死信队列
死信(dead message) 简单理解就是因为种种原因, ⽆法被消费的信息, 就是死信.
有死信, ⾃然就有死信队列. 当消息在⼀个队列中变成死信之后,它能被重新被发送到另⼀个交换器
中,这个交换器就是DLX( Dead Letter Exchange ), 绑定DLX的队列, 就称为死信队列(Dead
Letter Queue,简称DLQ).
消息变成死信⼀般是由于以下⼏种情况:
消息被拒绝( Basic.Reject/Basic.Nack ),并且设置 requeue 参数为 false.
消息过期.
队列达到最⼤⻓度.
添加配置
spring: application: name: rabbit-extensions-demo rabbitmq: addresses: amqp://study:study@47.98.109.138:5672/extension
常量类
public class Constants {
//死信
public static final String NORMAL_QUEUE = "normal.queue";
public static final String NORMAL_EXCHANGE = "normal.exchange";
public static final String DL_QUEUE = "dl.queue";
public static final String DL_EXCHANGE= "dl.exchange";
}
声明队列和交换机并绑定二者关系
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class DLConfig {
//正常的交换机和队列
@Bean("normalQueue")
public Queue normalQueue(){
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DL_EXCHANGE)
.deadLetterRoutingKey("dlx")
.build();
}
@Bean("normalExchange")
public DirectExchange normalExchange(){
return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();
}
@Bean("normalBinding")
public Binding normalBinding(@Qualifier("normalQueue") Queue queue, @Qualifier("normalExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
}
//死信交换机和队列
@Bean("dlQueue")
public Queue dlQueue(){
return QueueBuilder.durable(Constants.DL_QUEUE).build();
}
@Bean("dlExchange")
public DirectExchange dlExchange(){
return ExchangeBuilder.directExchange(Constants.DL_EXCHANGE).build();
}
@Bean("dlBinding")
public Binding dlBinding(@Qualifier("dlQueue") Queue queue, @Qualifier("dlExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("dlx").noargs();
}
}
死信--消息过期
给队列设置TTL

编写生产消息代码
@RequestMapping("/dl")
public String dl() {
System.out.println("dl...");
//发送普通消息
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "dl test...");
System.out.printf("%tc 消息发送成功 \n", new Date());
return "消息发送成功";
}
编写消费消息代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
import java.util.Date;
@Component
public class DLListener {
@RabbitListener(queues = Constants.DL_QUEUE)
public void dlHandMessage(Message message, Channel channel) throws Exception {
//消费者逻辑
System.out.printf("[dl.queue] %tc 接收到消息: %s, deliveryTag: %d \n", new Date(), new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
}
}
观察现象



我们可以看到,消息在10秒后过期,从normal队列进入到了死信队列,消息进入到死信队列后被消费。
死信--消息超过队列最大长度
设置队列的最大长度

编写生产消息代码
@RequestMapping("/dl")
public String dl() {
//测试队列长度
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "dl test..."+i);
}
return "消息发送成功";
}
编写消费消息代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
import java.util.Date;
@Component
public class DLListener {
@RabbitListener(queues = Constants.DL_QUEUE)
public void dlHandMessage(Message message, Channel channel) throws Exception {
//消费者逻辑
System.out.printf("[dl.queue] %tc 接收到消息: %s, deliveryTag: %d \n", new Date(), new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
}
}


此时我们可以看到,给队列设置了最大长度为10,但是队列接收到了20条消息,就会导致前10条消息变成死信。
死信--消息被拒绝
编写生产消息代码
@RequestMapping("/dl")
public String dl() {
System.out.println("dl...");
//发送普通消息
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "dl test...");
System.out.printf("%tc 消息发送成功 \n", new Date());
return "消息发送成功";
}
编写消费消息代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class DLListener {
@RabbitListener(queues = Constants.NORMAL_QUEUE)
public void handMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//消费者逻辑
System.out.printf("[normal.queue]接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
//进行业务逻辑处理
System.out.println("业务逻辑处理");
int num = 3/0;
System.out.println("业务处理完成");
//肯定确认
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
//否定确认
channel.basicNack(deliveryTag, false, false); //requeue为false, 该消息成为死信
}
}
}
可以看到,normal队列中的消息在被消费时因为发生了异常而执行到了拒绝消息的代码,而且设置了消息不重新入队,导致消息变成了死信,进而进入到了死信队列。

面试题
1.死信队列的概念
死信(Dead Letter)是消息队列中的⼀种特殊消息, 它指的是那些⽆法被正常消费或处理的消息. 在消息队列系统中, 如RabbitMQ, 死信队列⽤于存储这些死信消息。
2.死信的来源
消息过期: 消息在队列中存活的时间超过了设定的TTL
消息被拒绝: 消费者在处理消息时, 可能因为消息内容错误, 处理逻辑异常等原因拒绝处理该消息. 如果拒绝时指定不重新⼊队(requeue=false), 消息也会成为死信.
队列满了: 当队列达到最⼤⻓度, ⽆法再容纳新的消息时, 新来的消息会被处理为死信.
3.死信的应用场景
对于RabbitMQ来说, 死信队列是⼀个⾮常有⽤的特性. 它可以处理异常情况下,消息不能够被消费者正确消费⽽被置⼊死信队列中的情况, 应⽤程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况, 进⽽可以改善和优化系统.
⽐如: ⽤⼾⽀付订单之后, ⽀付系统会给订单系统返回当前订单的⽀付状态
为了保证⽀付信息不丢失, 需要使⽤到死信队列机制. 当消息消费异常时, 将消息投⼊到死信队列中, 由订单系统的其他消费者来监听这个队列, 并对数据进⾏处理(⽐如发送⼯单等,进⾏⼈⼯确认).
场景的应⽤场景还有:
• 消息重试:将死信消息重新发送到原队列或另⼀个队列进⾏重试处理.
• 消息丢弃:直接丢弃这些⽆法处理的消息,以避免它们占⽤系统资源.
• ⽇志收集:将死信消息作为⽇志收集起来,⽤于后续分析和问题定位.
RabbitMQ高级特性之延迟队列
延迟队列
延迟队列(Delayed Queue),即消息被发送以后, 并不想让消费者⽴刻拿到消息, ⽽是等待特定时间后,消费者才能拿到这个消息进⾏消费
应用场景
延迟队列的使⽤场景有很多, ⽐如:
智能家居: ⽤⼾希望通过⼿机远程遥控家⾥的智能设备在指定的时间进⾏⼯作. 这时候就可以将⽤⼾指令发送到延迟队列, 当指令设定的时间到了再将指令推送到智能设备.
⽇常管理: 预定会议后,需要在会议开始前⼗五分钟提醒参会⼈参加会议
⽤⼾注册成功后, 7天后发送短信, 提⾼⽤⼾活跃度等
RabbitMQ本⾝没有直接⽀持延迟队列的的功能, 但是可以通过前⾯所介绍的TTL+死信队列的⽅式组合模拟出延迟队列的功能.
假设⼀个应⽤中需要将每条消息都设置为10秒的延迟, ⽣产者通过 normal_exchange 这个交换器将发送的消息存储在 normal_queue 这个队列中. 消费者订阅的并⾮是 normal_queue 这个队列, ⽽
是 dlx_queue 这个队列. 当消息从 normal_queue 这个队列中过期之后被存⼊ dlx_queue 这个
队列中,消费者就恰巧消费到了延迟10秒的这条消息.

TTL+死信队列 实现延迟队列
添加配置
spring: application: name: rabbit-extensions-demo rabbitmq: addresses: amqp://study:study@47.98.109.138:5672/extension
常量类
public class Constants {
//死信
public static final String NORMAL_QUEUE = "normal.queue";
public static final String NORMAL_EXCHANGE = "normal.exchange";
public static final String DL_QUEUE = "dl.queue";
public static final String DL_EXCHANGE= "dl.exchange";
}
声明队列和交换机并绑定二者关系
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class DLConfig {
//正常的交换机和队列
@Bean("normalQueue")
public Queue normalQueue(){
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DL_EXCHANGE)
.deadLetterRoutingKey("dlx")
.build();
}
@Bean("normalExchange")
public DirectExchange normalExchange(){
return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();
}
@Bean("normalBinding")
public Binding normalBinding(@Qualifier("normalQueue") Queue queue, @Qualifier("normalExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
}
//死信交换机和队列
@Bean("dlQueue")
public Queue dlQueue(){
return QueueBuilder.durable(Constants.DL_QUEUE).build();
}
@Bean("dlExchange")
public DirectExchange dlExchange(){
return ExchangeBuilder.directExchange(Constants.DL_EXCHANGE).build();
}
@Bean("dlBinding")
public Binding dlBinding(@Qualifier("dlQueue") Queue queue, @Qualifier("dlExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("dlx").noargs();
}
}
编写生产消息代码
@RequestMapping("/delay")
public String delay() {
System.out.println("delay...");
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "ttl test 10s...", message -> {
message.getMessageProperties().setExpiration("10000"); //单位: 毫秒, 过期时间为10s
return message;
});
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "ttl test 30s...", message -> {
message.getMessageProperties().setExpiration("30000"); //单位: 毫秒, 过期时间为30s
return message;
});
System.out.printf("%tc 消息发送成功 \n", new Date());
return "消息发送成功";
}
编写消费消息代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
import java.util.Date;
@Component
public class DLListener {
@RabbitListener(queues = Constants.DL_QUEUE)
public void dlHandMessage(Message message, Channel channel) throws Exception {
//消费者逻辑
System.out.printf("[dl.queue] %tc 接收到消息: %s, deliveryTag: %d \n", new Date(), new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
}
}

此时我们可以看到,基于TTL+死信队列实现出来了 延迟队列 的效果,但是这样就没问题了吗?
修改生产消息代码
@RequestMapping("/delay")
public String delay() {
System.out.println("delay...");
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "ttl test 30s...", message -> {
message.getMessageProperties().setExpiration("30000"); //单位: 毫秒, 过期时间为30s
return message;
});
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE, "normal", "ttl test 10s...", message -> {
message.getMessageProperties().setExpiration("10000"); //单位: 毫秒, 过期时间为10s
return message;
});
System.out.printf("%tc 消息发送成功 \n", new Date());
return "消息发送成功";
}

此时我们看到,设置TTL为10秒的消息居然在30秒后才进入死信队列,这是为什么呢?
是因为RabbitMQ检查消息的TTL是在消息发送给消费方的时候进行检测的,而什么时候发送给发送方又根据队头消息的TTL,所以这就是问题所在,也是TTL+死信队列实现延迟队列所存在的问题
那这个问题在上一篇ttl的时候就说过了,这里依然是个问题,虽然设置队列的ttl不会有这个问题,但是设置队列ttl我们针对不同延迟时间就需要创建多个队列,这是不太合理的,所以针对这个问题,我们有一个延迟队列的插件可以使用
延迟队列插件
延迟队列插件,会给我们提供一个特殊的交换机,来完成我们的延迟功能
这是我们插件的下载地址
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
我们需要找到ez文件并下载,但是注意这里的版本要与你的RabbitMQ版本可以匹配,否则之后会出现问题

那插件下完后,我们要找到对应目录,下载插件

上面两个目录,我们可以任选一个下载即可,如果没有这个目录,我们手动创建
然后把下载的ez文件,copy到这个目录中即可,然后我们可以使用命令 rabbitmq-plugins list 来查看插件列表,看看我们有没有成功放进去,但是注意,即使我们成功放进去并成功显示了,也可能会出错,这就可能是你们下载的RabbitMQ版本与整个延迟插件的版本不匹配,重新下载其他版本即可
然后我们启动插件 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
之后重启服务 service rabbitmq-server restart
在没有发生错误的情况下,我们就发现我们会多了一个默认的交换机

此时我们代码中就不需要声明普通交换机了而是直接使用默认交换机即可
我们生产者代码是需要改一下的,我们需要调用一个方法来设置延迟时间
@RequestMapping("/delay2")
public String delay2() {
//发送带ttl的消息
rabbitTemplate.convertAndSend(Constant.DELAYED_EXCHANGE_NAME, "delayed",
"delayed test 20s..."+new Date(), messagePostProcessor -> {
messagePostProcessor.getMessageProperties().setDelayLong(20000L);
return messagePostProcessor;
});
rabbitTemplate.convertAndSend(Constant.DELAYED_EXCHANGE_NAME, "delayed",
"delayed test 10s..."+new Date(), messagePostProcessor -> {
messagePostProcessor.getMessageProperties().setDelayLong(10000L); //设置延迟时间
return messagePostProcessor;
});
return "发送成功!";
}
此时我们就可以在10s正确接收一个消息,在20s正确接收另一个消息

两者的区别:
1.通过TTL+死信
优点:比较灵活,不需要我们额外引入插件
缺点:我们设置消息TTL的时候可能会出现顺序的问题,而且我们需要多创建死信队列和死信交换机,完成一些绑定,增加了系统的复杂性
2.基于插件实现的延迟队列
优点:通过插件能够简化延迟消息的实现,并且避免了时序问题
缺点:需要依赖插件,不同版本RabbitMQ需要不同版本插件,有运维工作
RabbitMQ高级特性之事务
事务
RabbitMQ是基于AMQP协议实现的,该协议实现了事务机制,所以RabbitMQ也支持事务机制,他的事务允许开发者确保消息的发送和接收时原子性的,要么全部成功,要么全部失败
我们设置事务有三步,首先就是开启事务

因为我们是针对rabbitTemplate来作设置的,所以会影响此rabbitTemplate的所有消息,这里我们新开了一个,然后我们使用时要加一个注解

最后一步我们需要加上事务管理

这样我们就成功开启了事务
这三步是必不可少的,缺少一步,都无法成功开启事务
RabbitMQ高级特性之消息分发
消息分发
RabbitMQ队列拥有多个消费者时, 队列会把收到的消息分派给不同的消费者. 每条消息只会发送给订阅列表⾥的⼀个消费者. 这种⽅式⾮常适合扩展, 如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可.
默认情况下, RabbitMQ是以轮询的⽅法进⾏分发的, ⽽不管消费者是否已经消费并已经确认了消息. 这种⽅式是不太合理的, 试想⼀下, 如果某些消费者消费速度慢, ⽽某些消费者消费速度快, 就可能会导致某些消费者消息积压, 某些消费者空闲, 进⽽应⽤整体的吞吐量下降.
如何处理呢? 我们可以channel.basicQos(int prefetchCount) ⽅法, 来限制当前信道上的消费者所能保持的最⼤未确认消息的数量.
⽐如: 消费端调⽤了 channelbasicQos(5) , RabbitMQ会为该消费者计数, 发送⼀条消息计数+1, 消费⼀条消息计数-1, 当达到了设定的上限, RabbitMQ就不会再向它发送消息了,直到消费者确认了某条消息.类似TCP/IP中的"滑动窗⼝".
添加配置
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://study:study@47.98.109.138:5672/extension
listener:
simple:
acknowledge-mode: manual #消息接收确认
prefetch: 5
常量类
public class Constants {
//限流
public static final String QOS_QUEUE = "qos.queue";
public static final String QOS_EXCHANGE = "qos.exchange";
}
声明配置和交换机并绑定二者关系
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import rabbitextensionsdemo.constant.Constants;
@Configuration
public class QosConfig {
@Bean("qosQueue")
public Queue qosQueue(){
return QueueBuilder.durable(Constants.QOS_QUEUE).build();
}
@Bean("qosExchange")
public Exchange qosExchange(){
return ExchangeBuilder.directExchange(Constants.QOS_EXCHANGE).build();
}
@Bean("qosBinding")
public Binding qosBinding(@Qualifier("qosQueue") Queue queue, @Qualifier("qosExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("qos").noargs();
}
}
限流
如下使⽤场景:
订单系统每秒最多处理5000请求, 正常情况下, 订单系统可以正常满⾜需求
但是在秒杀时间点, 请求瞬间增多, 每秒1万个请求, 如果这些请求全部通过MQ发送到订单系统, ⽆疑会把订单系统压垮.

RabbitMQ提供了限流机制, 可以控制消费端⼀次只拉取N个请求。
通过设置prefetchCount参数, 同时也必须要设置消息应答⽅式为⼿动应答。
prefetchCount: 控制消费者从队列中预取(prefetch)消息的数量, 以此来实现流控制和负载均衡。
编写生产消息代码
@RequestMapping("/qos")
public String qos() {
System.out.println("qos test...");
//发送普通消息
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(Constants.QOS_EXCHANGE, "qos", "qos test..."+i);
}
return "消息发送成功";
}
编写消费消息代码1
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class QosListener {
@RabbitListener(queues = Constants.QOS_QUEUE)
public void handMessage(Message message, Channel channel) throws Exception {
//消费者逻辑
System.out.printf("111接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
}
}


可以看到,只消费了5条消息,且这5条消息未确定。
编写消费消息代码2
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class QosListener {
@RabbitListener(queues = Constants.QOS_QUEUE)
public void handMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
//消费者逻辑
System.out.printf("111接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
//肯定确认
channel.basicAck(deliveryTag,false);
}
}

此时我们可以看到,消息一次性被全都消费掉了。
负载均衡
我们也可以⽤此配置,来实现"负载均衡"
如下图所⽰, 在有两个消费者的情况下,⼀个消费者处理任务⾮常快, 另⼀个⾮常慢,就会造成⼀个消费者会⼀直很忙, ⽽另⼀个消费者很闲. 这是因为 RabbitMQ 只是在消息进⼊队列时分派消息. 它不考虑消费者未确认消息的数量。
更改配置
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://study:study@47.98.109.138:5672/extension
listener:
simple:
acknowledge-mode: manual #消息接收确认
prefetch: 1
编写消费消息代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import rabbitextensionsdemo.constant.Constants;
@Component
public class QosListener {
@RabbitListener(queues = Constants.QOS_QUEUE)
public void handMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//消费者逻辑
System.out.printf("111接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
Thread.sleep(2000);
//肯定确认
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
//否定确认
channel.basicNack(deliveryTag, false, true);
}
}
@RabbitListener(queues = Constants.QOS_QUEUE)
public void handMessage2(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//消费者逻辑
System.out.printf("222接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
Thread.sleep(1000);
//肯定确认
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
//否定确认
channel.basicNack(deliveryTag, false, true);
}
}
}

我们可以看到看到,消费者2消费消息的速度是消费者1消费消息的速度的2倍。
以上就是RabbitMQ的绝大多数的高级特性了,希望对你有帮助