注意:本文阅读前,需要你对RabbitMQ的基本使用有一定的了解,否则会有点吃力
死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。
那么死信是个什么东西呢?
“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
channel.basicNack
或 channel.basicReject
,并且此时requeue
属性被设置为false
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
这一部分将是本文的关键,需要重点掌握
如何配置死信队列呢?其实很简单,大概可以分为以下步骤:
这里有一些注意点需要声明:
有了上面知识作为基础,接下来我们就开始编码实战
场景1:消息被消费者消费时,业务代码发生异常,进入catch调用basicNack方法,即消息被否定确认
场景2:订单消息迟迟没有被支付,超时了,即超过设置的TTL时间了
场景3:队列装不下新的消息了
这里我省略了项目搭建过程,直接上核心代码,最后会附上完整代码的,思路如下:
核心配置类如下,注释比较详细:
package org.wujiangbo.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* RabbitMQ 的配置类
* 作用:声明交换机、队列以及绑定关系
*/
@Configuration
public class RabbitmqConfig {
/***********************************************************************************
* 声明:普通队列、普通交换机,以及绑定关系
***********************************************************************************/
//创建普通队列
@Bean
public Queue normalQueue() {
Map<String, Object> arguments = new HashMap<>(2);
/**
* 设置死信相关参数
* x-dead-letter-exchange和x-dead-letter-exchange是固定写法
*/
// 绑定死信交换机
arguments.put("x-dead-letter-exchange", "deadExchange");
// 绑定死信的路由key
arguments.put("x-dead-letter-routing-key", "deadRoutingKey");
return new Queue("normalQueue", true, false,false, arguments);
}
//创建普通交换机
@Bean
public Exchange normalExchange(){
return ExchangeBuilder.topicExchange("normalExchange").durable(true).build();
}
//绑定普通队列和普通交换机
@Bean
public Binding normalBind(){
return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normalRoutingKey").noargs();
}
/***********************************************************************************
* 声明:死信队列、死信交换机,以及绑定关系
***********************************************************************************/
//创建死信队列(当普通队列中消息过期后,消息会转发到此队列)
@Bean
public Queue deadQueue(){
return new Queue("deadQueue",true,false,false);
}
//创建死信交换机
@Bean
public Exchange deadExchange(){
return ExchangeBuilder.topicExchange("deadExchange").durable(true).build();
}
//绑定死信队列和死信交换机
@Bean
public Binding deadBind(){
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("deadRoutingKey").noargs();
}
//初始化RabbitAdmin对象
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
// 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
rabbitAdmin.setAutoStartup(true);
//下面设置目的:项目启动时,就创建交换机和队列,不用等到生产者发消息时才创建
//创建交换机
rabbitAdmin.declareExchange(deadExchange());
rabbitAdmin.declareExchange(normalExchange());
//创建对列
rabbitAdmin.declareQueue(deadQueue());
rabbitAdmin.declareQueue(normalQueue());
return rabbitAdmin;
}
}
server:
port: 8001
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
# 发送确认
publisher-confirms: true
# 路由失败回调
publisher-returns: true
template:
# 交换机无法根据自身类型和路由键找到一个符合条件的队列时的处理方式
#true:RabbitMQ会调用Basic.Return命令将消息返回给生产者
#false:RabbitMQ会把消息直接丢弃
mandatory: true
listener:
simple:
# 每次从RabbitMQ获取的消息数量
prefetch: 1
default-requeue-rejected: false #被拒绝的消息是否重新回队列,默认:true
# 每个队列启动的消费者数量
concurrency: 1
# 每个队列最大的消费者数量
max-concurrency: 1
# 签收模式为手动签收-那么需要在代码中手动ACK
acknowledge-mode: manual
package org.wujiangbo.controller;
import com.alibaba.fastjson.JSONObject;
import org.wujiangbo.dto.OrderDto;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @desc 订单API接口
* @author wujiangbo
* @date 2022-04-02 09:40
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/addOrder/{userId}")
public String addOrder(@PathVariable("userId") Long userId){
System.out.println("开始下单啦.........");
/***********************************************************************************
* 1、开始封装订单消息(模拟数据)
***********************************************************************************/
OrderDto dto = new OrderDto();
dto.setId(UUID.randomUUID().toString().replace("-", ""));//订单编号
dto.setOrderMoney(800D);//订单金额
dto.setUserId(userId);//用户ID
//订单状态(0:待支付;1:已支付;2:已失效(超时))
/**
* 1、提交订单,默认状态为0,待支付
* 2、如果用户在指定的TTL时间内支付成功了,那么就将状态改为1
* 3、如果用户在指定的TTL时间内未支付成功,那么就将状态改为2
*/
dto.setOrderStatus("0");
/***********************************************************************************
* 2、发送带有过期时间的订单消息到RabbitMQ
***********************************************************************************/
rabbitTemplate.convertAndSend(
"normalExchange", //普通交换机名称
"normalRoutingKey", //正常路由键
JSONObject.toJSONString(dto), //消息
new MessagePostProcessor() { //定义消息属性处理类
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
/**
* 设置过期时间TTL(单位:毫秒)
* 模拟:订单5秒内还未被消费,那么就进入死信队列
*/
messageProperties.setExpiration("5000");
//设置持久化
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}
});
return "下单成功";
}
}
package org.wujiangbo.consumer;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @desc 订单消费者:专门处理订单消息
* @author wujiangbo
* @date 2022-04-02 10:16
*/
@Component
public class OrderConsumer {
/**
* 监听订单队列
*/
@RabbitListener(queues = {"normalQueue"})
public void handleOrderQueue(Message message, Channel channel) throws IOException {
System.out.println("订单消费者类,拿到消息:" + new String(message.getBody()));
try {
//开始处理消息
try {
//模拟处理消息花费时间
Thread.sleep(3000);
int i = 1/0;//模拟:处理消息时发生了某种异常
} catch (InterruptedException e) {
e.printStackTrace();
}
//消息处理完毕,一定要记得手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
//发生异常,进行NACK
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
/**
* 监听死信队列
*/
@RabbitListener(queues = {"deadQueue"})
public void handleDeadQueue(Message message, Channel channel) throws IOException {
System.out.println("拿到死信消息:" + new String(message.getBody()));
/**
* 这里取到消息后,根据实际业务规则,进行处理即可
*/
try {
//模拟处理消息花费时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//最后手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
浏览器访问:http://localhost:8001/order/addOrder/123
package org.wujiangbo.consumer;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @desc 订单消费者:专门处理订单消息
* @author wujiangbo
* @date 2022-04-02 10:16
*/
@Component
public class OrderConsumer {
/**
* 监听订单队列
*/
//@RabbitListener(queues = {"normalQueue"})
//public void handleOrderQueue(Message message, Channel channel) throws IOException {
// System.out.println("订单消费者类,拿到消息:" + new String(message.getBody()));
//
// try {
// //开始处理消息
// try {
// //模拟处理消息花费时间
// Thread.sleep(3000);
// int i = 1/0;//模拟:处理消息时发生了某种异常
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// //消息处理完毕,一定要记得手动ACK
// channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
// } catch (Exception e){
// //发生异常,进行NACK
// channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
// }
//}
/**
* 监听死信队列
*/
@RabbitListener(queues = {"deadQueue"})
public void handleDeadQueue(Message message, Channel channel) throws IOException {
System.out.println("拿到死信消息:" + new String(message.getBody()));
/**
* 这里取到消息后,根据实际业务规则,进行处理即可
*/
try {
//模拟处理消息花费时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//最后手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
同样的场景,只是假如现在消费者这边网络不佳,没有及时获取消息进行消费,所以消息就会在队列里停留超时
浏览器访问:http://localhost:8001/order/addOrder/123
控制台打印结果:
package org.wujiangbo.consumer;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @desc 订单消费者:专门处理订单消息
* @author wujiangbo
* @date 2022-04-02 10:16
*/
@Component
public class OrderConsumer {
/**
* 监听订单队列
*/
@RabbitListener(queues = {"normalQueue"})
public void handleOrderQueue(Message message, Channel channel) throws IOException {
System.out.println("订单消费者类,拿到消息:" + new String(message.getBody()));
try {
//开始处理消息
try {
//模拟处理消息花费时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消息处理完毕,一定要记得手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
//发生异常,进行NACK
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
/**
* 监听死信队列
*/
@RabbitListener(queues = {"deadQueue"})
public void handleDeadQueue(Message message, Channel channel) throws IOException {
System.out.println("拿到死信消息:" + new String(message.getBody()));
/**
* 这里取到消息后,根据实际业务规则,进行处理即可
*/
try {
//模拟处理消息花费时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//最后手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
在创建普通队列的时候需要手动指定一下队列的长度,如下:
//创建普通队列
@Bean
public Queue normalQueue() {
Map<String, Object> arguments = new HashMap<>(2);
/**
* 设置死信相关参数
* x-max-length、x-dead-letter-exchange、x-dead-letter-exchange等参数是固定写法
*/
//设置队列长度
arguments.put("x-max-length", 2);
// 绑定死信交换机
arguments.put("x-dead-letter-exchange", "deadExchange");
// 绑定死信的路由key
arguments.put("x-dead-letter-routing-key", "deadRoutingKey");
return new Queue("normalQueue", true, false,false, arguments);
}
同样的场景,只是我手动的设置了队列的长度
浏览器访问:http://localhost:8001/order/addOrder/123,需要快速多次点击下单场景,将队列撑爆
控制台打印结果:
从测试结果可以看出:
测试符合预期效果,测试成功,下面是流程图:
那么“死信”被丢到死信队列中后,会发生什么变化呢?
如果队列配置了参数 x-dead-letter-routing-key
的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。
举个例子大家就明白了:
如果原有消息的路由key是A
,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key
,则该消息成为死信后,将保留原有的路由keyA
,如果配置了该参数,并且值设置为B
,那么该消息成为死信后,路由key将会被替换为B
,然后被抛到死信交换机中。
另外,由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称
消息的Header中,也会添加很多奇奇怪怪的字段,修改一下上面的代码,在死信队列的消费者中添加一行日志输出:
System.out.println("properties===" + message.getMessageProperties());
然后再运行测试下,就会打印如下信息了:
properties===MessageProperties [headers={x-first-death-exchange=normalExchange, x-death=[{reason=maxlen, original-expiration=5000, count=1, exchange=normalExchange, time=Sat Apr 02 11:51:32 CST 2022, routing-keys=[normalRoutingKey], queue=normalQueue}], x-first-death-reason=maxlen, x-first-death-queue=normalQueue}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=deadExchange, receivedRoutingKey=deadRoutingKey, deliveryTag=1, consumerTag=amq.ctag-jsUyzKkC6_bQCr0i6Se5VA, consumerQueue=deadQueue]
Header中看起来有很多信息,实际上并不多,只是值比较长而已。下面就简单说明一下Header中的值:
字段名 | 含义 |
---|---|
x-first-death-exchange | 第一次被抛入的死信交换机的名称 |
x-first-death-reason | 第一次成为死信的原因,rejected :消息在重新进入队列时被队列拒绝,由于default-requeue-rejected 参数被设置为false 。expired :消息过期。maxlen : 队列内消息数量超过队列最大容量 |
x-first-death-queue | 第一次成为死信前所在队列名称 |
x-death | 历次被投入死信交换机的信息列表,同一个消息每次进入一个死信交换机,这个数组的信息就会被更新 |
通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?
使用原则:
一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了
比如:订单超时自定取消订单
死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。
总结一下死信消息的生命周期:
以上代码已上传gitee:https://gitee.com/colinWu_java/RabbitMQ_Demo.git