前言
记录我学习的 RabbitMQ 的高级特性。包括:生产端 和 消费端的消息可靠性、死信队列、延迟队列 和 TTL。提示:以下是本篇文章正文内容,下面案例可供参考
一、生产者可靠性
1 生产者消息传递的可靠性
在使用 RabbitM 的时候,作为消息的生产者(发送者),我们希望杜绝任何情况下的消息丢失和投递失败。对此, RabbitMQ 提供了两种方法来确保投递过程的正确性。
- confirm 确认模式
- return 退回模式
1.1 模式介绍
RabbitMQ 的整个消息投递的过程可以划分为下面的步骤:
producer --> rabbitmq broker --> exchange --> queue --> consumer
- 确认模式:消息从产生(producer)到 exchange(交换机)这一部分,若是出现问题,会返回一个 confirmCallBack;
- 退回模式:消息从 exchange(交换机)到 queue(队列)这一部分,消息投递失败,会返回一个 returnCallBack。
我们就是 通过 这两个 Callback 确保消息投递的可靠性。
1.2 确认模式
- 确认模式开启:ConnectionFactory 中开启 publisher-confirms=“true”
- 在 rabbitTemplate 定义 ConfirmCallBack 回调函数.
- 发送消息,若是在消息投递到 exchange 之前出错,我们的回调函数的逻辑就会执行,进行进一步处理。
/**
* 确认模式:
* 步骤:
* 1. 确认模式开启:ConnectionFactory中开启publisher-confirms="true"
* 2. 在rabbitTemplate定义ConfirmCallBack回调函数
*/
// 定义回调函数
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 相关配置信息
* @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败
* @param cause 失败原因
*/
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm方法被执行了....");
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
1.2.1 示例代码
通过 spring 整合 RabbitMQ 做示范。
1.2.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>rabbitmq-return-firm</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 引入依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
</project>
1.2.3 RabbitMQ 配置
rabbitmq.host=192.168.191.130
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
1.2.4 spring 整合配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入 rabbitmq 的配置文件 -->
<context:property-placeholder location="classpath:rabbitmq/rabbitmq.properties"/>
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true"
publisher-returns="true"/>
<!-- 定义交换机、队列 -->
<rabbit:admin connection-factory="connectionFactory"/>
<rabbit:queue id="test_confirm_queue" name="test_confirm_queue" auto-declare="true"></rabbit:queue>
<rabbit:direct-exchange name="test_confirm_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding queue="test_confirm_queue" key="item.confirm"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
1.2.5 测试类
package com.kaikeba.confirm;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class RabbitConfirmTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testConfirm() throws InterruptedException {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm方法被执行了....");
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
rabbitTemplate.convertAndSend("test_confirm_exchange", "item.confirm", "whovwhuhuwf ");
//
Thread.sleep(2000);
}
}
1.2.6 测试结果
● 正常传递
● 我们把交换机改错,然后再传递
1.3 退回模式
当消息发送给Exchange后,Exchange路由到Queue失败时才会执行 ReturnCallBack
- 开启回退模式:publisher-returns=“true”;
- 设置ReturnCallBack,复写对应方法;
- 设置Exchange处理消息失败的模式:setMandatory。
/**
* 回退模式: 当消息发送给Exchange后,Exchange路由到Queue失败是 才会执行 ReturnCallBack
* 步骤:
* 1. 开启回退模式:publisher-returns="true"
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息失败的模式:setMandatory
* 1. 如果消息没有路由到Queue,则丢弃消息(默认)
* 2. 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn() {
// 设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
// 设置 ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_confirm_exchange", "item.confirm",
"message returnback....");
}
1.3.1 测试类
其他的东西和确认模式的一样
package com.kaikeba.confirm;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class RabbitConfirmTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testReturn() {
// 设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
// 设置 ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println("message =" + message);
System.out.println("replyCode = " + replyCode);
System.out.println("replyText = " + replyText);
System.out.println("exchange = " + exchange);
System.out.println("routingKey = " + routingKey);
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_confirm_exchange", "item.confi--rm",
"message returnback....");
}
}
1.3.2 测试结果
二、消费端消息传递的可靠性
2.1 Consume Ack
ack 指的是 Acknowledge 确认;表示消费端收到消息后的确认方式,这种方式有三种:
- 自动确认:acknowledge=“none”;
- 手动确认:acknowledge=“manual”;
- 根据异常情况确认:acknowledge=“auto”;这种方式比较麻烦,不常用。
其中,自动确认是指当消息一旦被消费者 Consumer 接收到,就自动确认收到,并将相应的消息 message 从 RabbitMQ 的消息缓存中移除;但是在实际业务中,消息接收到之后,很有可能业务处理出现异常,那么代码中的消息处理失败,队列中的消息已经被移除,就会造成消息丢失。此时,我们就需要设置手动确认的逻辑;在业务处理成功之后,调用 channel.basicAck() 手动签收,如果出现异常。则调用 channel.basicNack() ,让消息自动重发。
Consumer Ack 机制:
- 设置手动签收: acknowledge=“manual” ;
- 让监听器类实现 ChannelAwareMessageListener 接口;
- 如果消息处理成功, 则调用channel的 basicAck()签收 ;
- 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer 。
2.2 示例代码
2.2.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>rabbit-ack-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 引入依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
</project>
2.2.2 rabbitmq.properties
rabbitmq.host=192.168.191.131
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
2.2.3 spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:rabibt="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入 rabbitmq 的配置文件 -->
<context:property-placeholder location="classpath:rabbitmq/rabbitmq.properties"/>
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true"
publisher-returns="true"/>
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
<context:component-scan base-package="com.kaikebai.listener"></context:component-scan>
<!-- acknowledge:设置手动签收 -->
<rabibt:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="ackListener" queue-names="test_confirm_queue"></rabbit:listener>
</rabibt:listener-container>
</beans>
2.2.4 AckListener.java
package com.kaikebai.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class AckListener implements ChannelAwareMessageListener {
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println(new String(message.getBody(), "UTF-8"));
System.out.println("逻辑处理业务");
// 模拟出异常
int i = 3/0;
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
e.printStackTrace();
// 拒绝签收
/**
* 第二个参数:true-表示可以拒绝多个消息,可以提高效率
* 第三个参数 requeue:true-表示消息重回队列
*/
channel.basicNack(deliveryTag, true, true);
// 下面的也是拒绝签收消息,但是不推荐使用,每次只能拒绝一个,
// channel.basicReject(deliveryTag, true);
}
}
public void onMessage(Message message) {
}
}
2.2.5 测试类
启动项目。试监听器开始工作
package com.kaikebai.listener;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class AckListererTest {
@Test
public void test() {
while (true) {
}
}
}
2.2.6 测试结果
● 异常结果
● 正常情况
2.3 Consumer 限流
MQ 可以进行“削峰填谷”,当在短时间出现大量数据时(如秒杀活动);我们可以将消息存放到消息队列中,然后系统从消息队列中,每次确定特定数量的消息进行消费,以此达到限流的目的。
Consumer 限流机制:
- 确保 Ack 机制是手动确认;
- listener-container 设置属性,
a. perfetch=1,表示消费端每次从 MQ 队列中取出一条消息进行消费,消费完毕之后,再去拉取下一条先息进行消费。
package com.kaikebai.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class QspListener implements ChannelAwareMessageListener {
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(1000);
//1.获取消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
//3. 签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
}
public void onMessage(Message message) {
}
}
三、 TTL
全称是 Time To Live ,消息过期时间设置。队列或消息设置过期时间的话,在规定时间过后,队列中的消息将被重新处理;处理方式有两种:
- 进入死信队列:若是该队列有对应的死信队列,则消息过期之后,会根据配置的死信队列信息,转发到对应的四星队列中;
- 直接丢弃:若是没有配置死信队列,则消息过期之后,江北直接丢弃。
管控台中设置队列TTL :
3.1 示例代码
3.1.1 配置文件
rabbitmq.properties
rabbitmq.host=192.168.191.132
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:contect="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入 rabbitmq 的配置文件 -->
<context:property-placeholder location="classpath:rabbitmq.properties"/>
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<!-- 定义交换机、队列 -->
<rabbit:admin connection-factory="connectionFactory"/>
<!-- 通配符模式 -->
<rabbit:queue name="test_ttl_queue" id="test_ttl_queue" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="test_ttl_exchange" id="test_ttl_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="ttl.#" queue="test_ttl_queue"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
3.1.2 测试代码
package com.kaikeba;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Correlation;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class TtlQueueTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* TTL:过期时间
* 1. 队列统一过期
*
* 2. 消息单独过期
*
*
* 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
* 队列过期后,会将队列所有消息全部移除。
* 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)
*
*/
/**
* 队列设置过期时间
*/
@Test
public void testQueueTtl() {
rabbitTemplate.convertAndSend("test_ttl_exchange", "ttl.hello", "测试TTL队列");
}
/**
* 设置消息过期时间
*/
@Test
public void testMessageTtl() {
// 消息处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws AmqpException {
return null;
}
public Message postProcessMessage(Message message, Correlation correlation) {
// 1. 设置 message 的过期时间
message.getMessageProperties().setExpiration("5000");
return message;
}
};
// 消息单独发送
rabbitTemplate.convertAndSend("test_ttl_queue", "ttl.hi", "消息单独过期时间测试", messagePostProcessor);
/*// 下面这种设置过期的方式是不生效的,因为,单独设置的不是第一个消息
for (int i = 0; i < 20; i++) {
if(i == 5) {
rabbitTemplate.convertAndSend("test_ttl_queue", "ttl.hi", "消息单独过期时间测试", messagePostProcessor);
} else {
rabbitTemplate.convertAndSend("test_ttl_queue", "ttl.hi", "消息单独过期时间测试");
}
}*/
}
}
3.2 小结
- 消息过期可以从队列和消息本身两个维度进行设置;队列层次设置时,整个队列中的消息都以这个过期时间设置为准;在消息层次设置时,只对当前单个消息有效;同时设置时,那一个设置的时间短,那一个起作用。
- 针对单个消息设置过期时间时,只有当这个消息是队列中的第一个消息时(排在队列的首位),这个设置才会生效。否则设置不会生效。
- 单独设置消息过期时间是通过 messagePostProcessor 中的 postProcessMessage 方法 来设置的
四、 死信队列
4.1 介绍
死信队列:DLX(Dead Letter Exchange 死信交换机),因为有的 MQ产品是没有交换机这个设计的,所以我们一般直接叫死信队列。当消息成为 Dead Message 之后,可以被重新发送到另一个交换机,这个交换机就是 DLX 。
消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队 列,requeue=false;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
队列绑定死信交换机:
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
4.2 示例代码
4.2.1 配置文件
rabbitmq.properties
rabbitmq.host=192.168.191.130
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:contect="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder>
<!-- 定义 RabbitMQ ConnectionFactory -->
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<rabbit:admin connection-factory="connectionFactory" />
<!-- 声明并绑定死信交换机、私信队列 -->
<rabbit:queue id="queue_dlx" name="queue_dlx" auto-declare="true"></rabbit:queue>
<rabbit:topic-exchange id="exchange_dlx" name="exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.#" queue="queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!-- 声明正常的队列 test_queue_dlx-->
<rabbit:queue id="test_queue_dlx" name="test_queue_dlx" auto-declare="true">
<!-- 绑定死信交换机 -->
<rabbit:queue-arguments>
<!-- x-dead-letter-exchange 死信交换机 -->
<entry key="x-dead-letter-exchange" value="exchange_dlx"></entry>
<!-- x-dead-letter-routing-key 发送给死信交换机的路由 key -->
<entry key="x-dead-letter-routing-key" value="dlx.hello"></entry>
<!-- 设置消息过期时间 -->
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
<!-- 设置队列的长度 -->
<entry key="x-max-length" value="10" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<!-- 正常的交换机 -->
<rabbit:topic-exchange name="test_exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
4.2.2 测试类
package com.kaikeba.dlx;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class DeadLetterExchangeTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息过期
*/
@Test
public void testDlxExpression() {
// 死信 -- 测试过期时间
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "死信队列,我是测试过期时间的!!!");
}
/**
* 数量超过
*/
@Test
public void testDlxMax() {
// 死信 -- 数量超过
for(int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "死信队列,我是测试过期时间的!!!");
}
}
/**
* 数量拒收
*/
@Test
public void testDlxRef() {
// 死信 -- 拒收
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.hehe", "死信队列,我是测试过期时间的!!!");
}
}
4.2.3 消费者代码
消费者代码是模拟消费端拒接消息的场景
4.2.3.1 DlxListener.java
package com.kaikeba.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class DlxListener implements ChannelAwareMessageListener {
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1.接收转换消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
System.out.println("处理业务逻辑...");
int i = 3/0;//出现错误
//3. 手动签收
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
//e.printStackTrace();
System.out.println("出现异常,拒绝接受");
//4.拒绝签收,不重回队列 requeue=false
channel.basicNack(deliveryTag,true,false);
}
}
public void onMessage(Message message) {
}
}
4.2.3.2 配置文件
rabbitmq.properties
rabbitmq.host=192.168.191.130
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:contect="http://www.springframework.org/schema/context"
xmlns:ra="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder>
<!-- 定义 RabbitMQ ConnectionFactory -->
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<rabbit:admin connection-factory="connectionFactory" />
<!-- 声明并绑定死信交换机、私信队列 -->
<rabbit:queue id="queue_dlx" name="queue_dlx" auto-declare="true"></rabbit:queue>
<rabbit:topic-exchange id="exchange_dlx" name="exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.#" queue="queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<context:component-scan base-package="com.kaikeba"></context:component-scan>
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener ref="dlxListener" queue-names="test_queue_dlx"></rabbit:listener>
</rabbit:listener-container>
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
4.2.3.3 测试类
package com.kaikeba.listener;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class DlsConsumerTest {
@Test
public void test() {
while (true) {
}
}
}
4.2.4 测试结果
- 交换机
- 交换机绑定关系
- 交换机
- 队列
test_queue_dlx 是正常交换机,我们设置的是10秒,10 秒之后,就转发到死信队列 queue_dlx。
五、 延迟队列
延迟队列:即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
我们模拟一个下单需求,来说明 RabbitMQ 的延时队列的实现
模拟需求:
- 下单后,30分钟未支付,取消订单,回滚库存。
- 新用户注册成功7天后,发送短信问候。
一般情况:
有的 MQ 产品是有延迟队列的,可以直接实现;
实现方式:
- 定时器
- 延迟队列
而我们的 RabbitMQ 中没有延迟队列的直接实现;但是我们可以通过 TTL + 死信队列 来达到延迟队列的效果。
我们把正常队列的消息过期时间设置为 30min;将消费投递到正常队列,30min 过后消息过期,转发到对应的死信队列,我们的程序监听死信队列,并消费其中的消息。就达到了消息延迟 30min 的目的。
实现过程:
- 定义正常交换机(order_exchange)和队列(order_queue)
- 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
- 绑定,设置正常队列过期时间为30分钟
5.1 示例代码
5.1.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>rabbit-delay-producer</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 引入依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
</project>
5.1.2 配置文件
rabbitmq.xml
rabbitmq.host=192.168.191.128
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=admin
rabbitmq.virtual-host=/xzk
spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:contect="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder>
<!-- 定义 RabbitMQ ConnectionFactory -->
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<rabbit:admin connection-factory="connectionFactory" />
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
<!-- 延迟队列 -->
<!--
1. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
2. 定义正常交换机(order_exchange)和队列(order_queue)
3. 绑定,设置正常队列过期时间为 30 分钟。
-->
<!-- 1. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx) -->
<rabbit:queue id="order_queue_dlx" name="order_queue_dlx" auto-declare="true"></rabbit:queue>
<rabbit:topic-exchange name="order_exchange_dlx" id="order_exchange_dlx" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!-- 2. 定义正常交换机(order_exchange)和队列(order_queue) -->
<rabbit:queue id="order_queue" name="order_queue" auto-declare="true">
<!-- 3. 绑定死信队列,设置过期时间(为了测试,设置10秒) -->
<rabbit:queue-arguments>
<!-- 死信交换机 -->
<entry key="x-dead-letter-exchange" value="order_exchange_dlx"></entry>
<!-- 死信 routing key -->
<entry key="x-dead-letter-routing-key" value="dlx.order.cancel"></entry>
<!-- 过期时间 -->
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="order_exchange" id="order_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
</beans>
5.1.3 测试类
package com.kaikeba.delay.kaikeba.delay;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Date;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class DelayProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testDelay() {
// 1.发送订单消息。 将来是在订单系统中,下单成功后,发送消息
rabbitTemplate.convertAndSend("order_exchange", "order.msg","订单信息:" + new Date());
}
}
5.1.4 测试结果
- 交换机
- 队列
5.2 消费者
消费者监听死信队列,模拟延时效果
5.2.1 监听类
package com.kaikeba.delay;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class DelayListener implements ChannelAwareMessageListener {
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1.接收转换消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
System.out.println("处理业务逻辑...");
System.out.println("根据订单id查询其状态...");
System.out.println("判断状态是否为支付成功");
System.out.println("取消订单,回滚库存....");
//3. 手动签收
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
//e.printStackTrace();
System.out.println("出现异常,拒绝接受");
//4.拒绝签收,不重回队列 requeue=false
channel.basicNack(deliveryTag,true,false);
}
}
public void onMessage(Message message) {
}
}
5.2.2 配置文件
rabbitmq 的配置文件和生产者一样。
spring-rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:contect="http://www.springframework.org/schema/context"
xmlns:rabbir="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:rabbitmq.properties"></context:property-placeholder>
<!-- 定义 RabbitMQ ConnectionFactory -->
<!-- 定义 RabbitMQ 连接工厂 -->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<rabbit:admin connection-factory="connectionFactory" />
<!-- 定义 RabbitTemplate 对象,方便代码中操作数据 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
<context:component-scan base-package="com.kaikeba"></context:component-scan>
<rabbir:listener-container connection-factory="connectionFactory">
<rabbit:listener ref="delayListener" queue-names="order_queue_dlx"/>
</rabbir:listener-container>
</beans>
5.2.3 测试类
package com.kaikeba.delay;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class DelayListenerTest {
@Test
public void test() {
while (true) {
}
}
}