死信:无法被消费方消费掉的消息,称为死信。如果死信一直留在队列中,会导致一直被消费,却从不消费成功。所以 rabbitmq
专门开辟了一个来存放死信的队列,叫死信队列(DLX,dead-letter-exchange
)
basicNack()
或 basicReject()
,并且参数都是 requeue = false
,则消息会路由进死信队列TTL
存活时间,就是消费方在 TTL
时间之内没有消费,则消息会路由进死信队列x-max-length
最大消息数量且当前队列中的消息已经达到了这个数量,再次投递,消息将被挤掉,被挤掉的消息会路由进死信队列ACK
,当重复投递次数达到了设置的最大 retry
次数之后,消息也会投递到死信队列,但是内部的原理还是调用了 basicNack()
或 basicReject()
#开启rabbitmq的生产端重试机制,默认是false,默认重试 3 次
spring.rabbitmq.template.retry.enabled=true
#开启rabbitmq的消费端重试机制,默认是false,默认重试 3 次
spring.rabbitmq.listener.simple.retry.enabled=true
#设置重试的次数
spring.rabbitmq.listener.simple.retry.max-attempts=5
死信流程图
Exchange
,该 Exchange
根据路由键将消息路由到绑定的正常队列死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接收死信的交换机,所以可以为任何类型(Direct、Fanout、Topic
)。一般来说,会为每个业务队列分配一个独有的路由 key
,并对应的配置一个死信队列进行监听,也就是说,一般会为 每个重要的业务队列配置一个死信队列
SpringBoot
版本 2.0.6.RELEASE
Rabbitmq
版本 3.8.3
分别创建项目消息生产方,消息接收方两个项目
server.port=8080
#配置rabbitmq服务器
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#确认消息已发送到交换机
spring.rabbitmq.publisher-confirms=true
#确认消息已发送到队列
spring.rabbitmq.publisher-returns=true
@Configuration
public class DeadLetterConfig {
// --------------------------正常业务队列--------------------------
// 业务队列 A
@Bean
public Queue businessQueueA() {
Map<String, Object> args = new HashMap<>();
// x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
return new Queue(Constant.BUSINESS_QUEUE_A, true, false, false, args);
}
// 业务队列 B
@Bean
public Queue businessQueueB() {
Map<String, Object> args = new HashMap<>();
// x-dead-letter-exchange:这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key:这里声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_B_ROUTING_KEY);
return new Queue(Constant.BUSINESS_QUEUE_B, true, false, false, args);
}
// 业务队列的交换机
@Bean
public TopicExchange businessTopicExchange() {
return new TopicExchange(Constant.BUSINESS_EXCHANGE, true, false);
}
// 业务队列 A 与交换机绑定,并指定 Routing_Key
@Bean
public Binding businessBindingA() {
return BindingBuilder.bind(businessQueueA()).to(businessTopicExchange()).with(Constant.BUSINESS_QUEUE_A_ROUTING_KEY);
}
// 业务队列 B 与交换机绑定,并指定 Routing_Key
@Bean
public Binding businessBindingB() {
return BindingBuilder.bind(businessQueueB()).to(businessTopicExchange()).with(Constant.BUSINESS_QUEUE_B_ROUTING_KEY);
}
// --------------------------死信队列--------------------------
// 死信队列 A
@Bean
public Queue deadLetterQueueA() {
return new Queue(Constant.DEAD_LETTER_QUEUE_A);
}
// 死信队列 B
@Bean
public Queue deadLetterQueueB() {
return new Queue(Constant.DEAD_LETTER_QUEUE_B);
}
// 死信交换机
@Bean
public DirectExchange deadLetterDirectExchange() {
return new DirectExchange(Constant.DEAD_LETTER_EXCHANGE);
}
// 死信队列 A 与死信交换机绑定,并指定 Routing_Key
@Bean
public Binding deadLetterBindingA() {
return BindingBuilder.bind(deadLetterQueueA()).to(deadLetterDirectExchange()).with(Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
}
// 死信队列 B 与死信交换机绑定,并指定 Routing_Key
@Bean
public Binding deadLetterBindingB() {
return BindingBuilder.bind(deadLetterQueueB()).to(deadLetterDirectExchange()).with(Constant.DEAD_LETTER_QUEUE_B_ROUTING_KEY);
}
// --------------------------使用 RabbitAdmin 启动服务便创建交换机和队列--------------------------
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
// 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
rabbitAdmin.setAutoStartup(true);
// 创建死信交换机和对列
rabbitAdmin.declareExchange(deadLetterDirectExchange());
rabbitAdmin.declareQueue(deadLetterQueueA());
rabbitAdmin.declareQueue(deadLetterQueueB());
// 创建业务交换机和对列
rabbitAdmin.declareExchange(businessTopicExchange());
rabbitAdmin.declareQueue(businessQueueA());
rabbitAdmin.declareQueue(businessQueueB());
return rabbitAdmin;
}
}
A
,B
分别与 TopicExchange
类型的业务交换机绑定,指定路由 key
A
,B
分别与 DirectExchange
类型的 死信
交换机绑定,指定路由 key
(一旦成为死信,通过该 key
路由至死信队列中),代码如下Map<String, Object> args = new HashMap<>();
// x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
A
,B
分别与 DirectExchange
类型的死信交换机绑定,指定路由 key
arguments
具体参数如下
参数名 | 作用 |
---|---|
x-message-ttl | 发送到队列的消息在丢弃之前可以存活多长时间(毫秒) |
x-max-length | 队列最大长度 |
x-expires | 队列在被自动删除(毫秒)之前可以使用多长时间 |
x-max-length-bytes | 队列在开始从头部删除之前可以包含的就绪消息的总体大小 |
x-dead-letter-exchange | 设置队列溢出行为。这决定了在达到队列的最大长度时消息会发生什么。 有效值为drop-head或reject-publish。交换的可选名称,如果消息被拒绝或过期,将重新发布这些名称 |
x-dead-letter-routing-key | 可选的替换路由密钥,用于在消息以字母为单位时使用。如果未设置,将使用消息的原始路由密钥 |
x-max-priority | 队列支持的最大优先级数;如果未设置,队列将不支持消息优先级 |
x-queue-mode | 将队列设置为延迟模式,在磁盘上保留尽可能多的消息以减少内存使用;如果未设置,队列将保留内存缓存以尽快传递消息 |
x-queue-master-locator | 将队列设置为主位置模式,确定在节点集群上声明时队列主机所在的规则 |
x-overflow | 队列达到最大长度时,可选模式包括: drop-head , reject-publish 和 reject-publish-dlx . |
Constant
类public class Constant {
public static final String BUSINESS_EXCHANGE = "dead.letter.business.exchange";
public static final String BUSINESS_QUEUE_A = "dead.letter.business.queuea";
public static final String BUSINESS_QUEUE_B = "dead.letter.business.queueb";
public static final String BUSINESS_QUEUE_A_ROUTING_KEY = "dead.letter.business.queuea";
public static final String BUSINESS_QUEUE_B_ROUTING_KEY = "dead.letter.business.#";
public static final String DEAD_LETTER_EXCHANGE = "dead.letter.deadletter.exchange";
public static final String DEAD_LETTER_QUEUE_A_ROUTING_KEY = "dead.letter.deadletter.queuea";
public static final String DEAD_LETTER_QUEUE_B_ROUTING_KEY = "dead.letter.deadletter.queueb";
public static final String DEAD_LETTER_QUEUE_A = "dead.letter.deadletter.queuea";
public static final String DEAD_LETTER_QUEUE_B = "dead.letter.deadletter.queueb";
}
@Slf4j
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
/*设置开启Mandatory才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数*/
rabbitTemplate.setMandatory(true);
/*消息发送到Exchange的回调,无论成功与否*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
log.info("ConfirmCallback:" + "相关数据:" + correlationData);
log.info("ConfirmCallback:" + "确认情况:" + ack);
log.info("ConfirmCallback:" + "原因:" + cause);
});
/*消息从Exchange路由到Queue失败的回调*/
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("ReturnCallback:" + "消息:" + message);
log.info("ReturnCallback:" + "回应码:" + replyCode);
log.info("ReturnCallback:" + "回应信息:" + replyText);
log.info("ReturnCallback:" + "交换机:" + exchange);
log.info("ReturnCallback:" + "路由键:" + routingKey);
});
return rabbitTemplate;
}
}
@Controller
public class SendMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
// 死信队列接口
@GetMapping("/sendDeadLetterMessage1")
@ResponseBody
public String sendDeadLetterMessage1() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: dead.letter.business.exchange test message ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend(Constant.BUSINESS_EXCHANGE, Constant.BUSINESS_QUEUE_A_ROUTING_KEY, map);
return "消息已发送至rabbitmq server";
}
}
server.port=8081
#配置rabbitmq服务器
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.type=simple
#消费方消息确认:手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.default-requeue-rejected=false
业务队列手动接收确认消息
@Slf4j
@Component
public class DeadLetterAckReceiver {
// 业务队列手动确认消息
@RabbitListener(queues = "dead.letter.business.queuea")
@RabbitHandler
public void deadLetterReceiver1(@NotNull Message message, Channel channel) {
try {
// 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
log.info("拒绝签收...消息的路由键为:" + message.getMessageProperties().getReceivedRoutingKey());
} catch (Exception e) {
log.info("消息拒绝签收失败", e);
}
}
// 业务队列手动确认消息
@RabbitListener(queues = "dead.letter.business.queueb")
@RabbitHandler
public void deadLetterReceiver2(@NotNull Message message, Channel channel) {
try {
// 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
log.info("拒绝签收...消息的路由键为:" + message.getMessageProperties().getReceivedRoutingKey());
} catch (Exception e) {
log.info("消息拒绝签收失败", e);
}
}
}
死信队列中消息的消费
@Slf4j
@Component
public class DeadLetterReceiver {
@RabbitListener(queues = {"dead.letter.deadletter.queuea"})
@RabbitHandler
public void deadLetterConsumer1(@NotNull Message message, @NotNull Channel channel) {
String msg = message.toString();
String[] msgArray = msg.split("'");
Map<String, String> msgMap = MapStringToMapUtil.getStringMap(msgArray[1].trim());
String messageId = msgMap.get("messageId");
String messageData = msgMap.get("messageData");
String createTime = msgMap.get("createTime");
log.info("死信队列接收到的消息为:" + "MyAckReceiver messageId:" + messageId + " messageData:" + messageData + " createTime:" + createTime);
log.info("消费的主题消息来自:" + message.getMessageProperties().getConsumerQueue());
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
log.info("死信队列中消息的消费失败", e);
e.printStackTrace();
}
}
@RabbitListener(queues = {"dead.letter.deadletter.queueb"})
@RabbitHandler
public void deadLetterConsumer2(@NotNull Message message, @NotNull Channel channel) {
String msg = message.toString();
String[] msgArray = msg.split("'");
Map<String, String> msgMap = MapStringToMapUtil.getStringMap(msgArray[1].trim());
String messageId = msgMap.get("messageId");
String messageData = msgMap.get("messageData");
String createTime = msgMap.get("createTime");
log.info("死信队列接收到的消息为:" + "MyAckReceiver messageId:" + messageId + " messageData:" + messageData + " createTime:" + createTime);
log.info("消费的主题消息来自:" + message.getMessageProperties().getConsumerQueue());
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
log.info("死信队列中消息的消费失败", e);
e.printStackTrace();
}
}
}
定义好之后启动程序,SpringBoot
会读取容器中类型为 Queue
和 Exchange
的 bean
进行队列和交换机的初始化与绑定。当然也可以自己在 RabbitMQ
的管理后台进行手动创建与绑定
四个队列如下
1
)测试:调用 basicNack()
或 basicReject()
,并且参数 requeue = false
,来看看死信队列。分别启动两个项目,及 rabbitmq
的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1
发送端日志情况
2
)测试:队列设置了 x-max-length
最大消息数量且当前队列中的消息已经达到了这个数量,再次投递,消息将被挤掉,被挤掉的会进入死信队列
发送方代码变动之处
只需要启动发送方项目即可,及 rabbitmq
的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1
我们连续发送 4
次请求
x-max-length = 2
,队列最大长度容量只有 2
个4
次请求,会将先进入业务队列的 2
个请求丢弃进死信队列,后面来的 2
个请求进入业务队列3
)测试:消息过期,过了 TTL
存活时间会进入死信队列
发送方设置消息过期时间
只需要启动发送方项目即可,及 rabbitmq
的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1
5s
等待过期后
一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了
死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。总结一下死信消息的生命周期:
basicNack()
或 basicReject()
操作basicNack()
或 basicReject()
的消息由 RabbitMQ
投递到死信交换机中死信消息是 RabbitMQ
为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些 Exchange
和 Queue
想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费
参考:SpringBoot中RabbitMQ死信队列介绍和使用
参考:rabbitmq channel 参数详解
源码:https://gitee.com/chaojiangcj/rabbitma-deadletter