RabbitMQ 延迟队列与死信队列:原理、实现与实战
一、核心概念
1.1 死信(Dead Letter)
消息变成"死信"的三种条件:
| 触发条件 | 说明 |
|---|---|
| 消息被拒绝(reject/nack)且不重新入队 | 消费者明确表示不处理该消息 |
| 消息 TTL 过期 | 消息在队列中存活超过指定时间 |
| 队列达到最大长度 | 队列满了,最早的消息被挤出 |
1.2 死信队列(DLQ - Dead Letter Queue)
死信队列本质上就是一个普通队列,只是它被配置为接收其他队列产生的死信。
正常队列 ──(消息变成死信)──→ 死信交换机 ──→ 死信队列
作用:
- 收集无法正常消费的消息,避免消息丢失
- 便于人工排查、监控告警、后续补偿
1.3 延迟队列(Delay Queue)
延迟队列的含义是:消息发送后不立即被消费,而是等待指定时间后才投递给消费者。
RabbitMQ 原生不支持延迟队列,通常通过以下方式实现:
| 方案 | 原理 |
|---|---|
| TTL + 死信路由 | 消息放入设置了 TTL 的队列,过期后转发到死信队列(实际消费队列) |
| 延迟插件(rabbitmq_delayed_message_exchange) | 消息发到延迟交换机,由插件在到期后路由到目标队列 |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、TTL + 死信路由实现延迟队列
2.1 架构图
生产者
│
▼
[delay-exchange] ──route──→ [delay-queue](设置 TTL,无消费者)
│
(消息过期)
│
▼
[process-dlx-exchange] ──route──→ [process-queue]
│
▼
消费者处理
2.2 两种 TTL 设置方式
队列级别 TTL:队列中所有消息统一过期时间
java
@Bean
public Queue delayQueue() {
Map<String, Object> args = new HashMap<>();
// 队列中所有消息 30 秒后过期
args.put("x-message-ttl", 30000);
// 死信交换机
args.put("x-dead-letter-exchange", "process-dlx-exchange");
// 死信路由键
args.put("x-dead-letter-routing-key", "process");
return new Queue("delay-queue", true, false, false, args);
}
消息级别 TTL:每条消息可设置不同的过期时间
java
rabbitTemplate.convertAndSend("delay-exchange", "delay", payload, message -> {
// 该消息 10 秒后过期
message.getMessageProperties().setExpiration("10000");
return message;
});
注意:消息级别 TTL 有一个坑------RabbitMQ 只检查队头消息是否过期。如果队头消息 TTL 长、后面消息 TTL 短,后面的消息不会优先过期。这会导致延迟不准确。队列级别 TTL 没有此问题。
三、延迟插件实现延迟队列
3.1 安装插件
bash
# 下载插件(版本需匹配 RabbitMQ 版本)
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
3.2 原理
插件提供了一种新的交换机类型 x-delayed-message,消息发送时在 header 中指定延迟时间,交换机内部存储消息,到期后再路由到绑定的队列。
生产者
│ (header: x-delay = 5000)
▼
[delayed-exchange] (type: x-delayed-message)
│
│ (5秒后)
▼
[target-queue] → 消费者
3.3 优势对比
| 对比项 | TTL + 死信 | 延迟插件 |
|---|---|---|
| 灵活度 | 队列级别统一或消息级别(有排队问题) | 每条消息任意延迟,无排队问题 |
| 部署依赖 | 无需额外插件 | 需安装 rabbitmq_delayed_message_exchange |
| 性能 | 大量过期消息时有压力 | 插件内部用 Mnesia/ETS 管理,大量延迟消息也稳定 |
| 云服务兼容 | 阿里云 AMQP 支持 | 阿里云 AMQP 不一定支持,需确认 |
四、死信队列的独立使用
死信队列不只是延迟队列的中间环节,也可以独立作为"错误消息收集器"。
4.1 架构图
[business-queue] (设置 x-dead-letter-exchange)
│
│ 消费者 nack / reject / 超过重试次数
▼
[dlx-exchange] ──→ [dead-letter-queue]
│
▼
监控告警 / 人工处理 / 补偿消费者
4.2 配置
java
// 业务队列:消费失败的消息转发到死信交换机
@Bean
public Queue businessQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx-exchange");
args.put("x-dead-letter-routing-key", "dead");
// 可选:最大重试次数通过 Spring Retry 控制
return new Queue("business-queue", true, false, false, args);
}
// 死信交换机
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("dlx-exchange");
}
// 死信队列
@Bean
public Queue deadLetterQueue() {
return new Queue("dead-letter-queue", true);
}
// 绑定
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(dlxExchange())
.with("dead");
}
五、业务场景示例
场景 1:订单超时自动关闭
用户下单 → 发送延迟消息(30分钟)→ 到期后消费者检查订单状态
- 已支付:忽略
- 未支付:关闭订单,释放库存
场景 2:MQ 消费失败重试(指数退避)
第 1 次失败 → 延迟 3 秒重试
第 2 次失败 → 延迟 9 秒重试
第 3 次失败 → 延迟 27 秒重试
第 4 次失败 → 进入死信队列,人工处理
场景 3:事务未提交问题的兜底
消费者查不到数据 → 发送到延迟队列(3秒后重试)
- 重试时数据已提交:正常处理
- 重试 N 次仍查不到:进入死信队列
场景 4:定时通知
预约提醒:预约时间前 1 小时发送延迟消息
会员到期:到期前 3 天发送续费提醒
六、完整代码示例(Spring Boot + RabbitMQ)
6.1 配置类:延迟队列 + 死信队列全套
java
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMqConfig {
// ======================== 延迟队列相关 ========================
/**
* 延迟交换机(消息先到这里)
*/
@Bean
public DirectExchange delayExchange() {
return new DirectExchange("delay.exchange");
}
/**
* 延迟队列:无消费者,消息在此等待过期
* 过期后通过死信路由转发到实际处理队列
*/
@Bean
public Queue delayQueue() {
Map<String, Object> args = new HashMap<>();
// 队列消息统一 TTL:30 秒
args.put("x-message-ttl", 30000);
// 消息过期后转发到的交换机
args.put("x-dead-letter-exchange", "process.exchange");
// 转发时使用的路由键
args.put("x-dead-letter-routing-key", "process");
return new Queue("delay.queue", true, false, false, args);
}
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue())
.to(delayExchange())
.with("delay");
}
// ======================== 实际处理队列 ========================
@Bean
public DirectExchange processExchange() {
return new DirectExchange("process.exchange");
}
/**
* 实际处理队列:消费者监听此队列
* 消费失败的消息转入死信队列
*/
@Bean
public Queue processQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange");
args.put("x-dead-letter-routing-key", "dead");
return new Queue("process.queue", true, false, false, args);
}
@Bean
public Binding processBinding() {
return BindingBuilder.bind(processQueue())
.to(processExchange())
.with("process");
}
// ======================== 死信队列 ========================
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("dlx.exchange");
}
/**
* 死信队列:收集所有无法正常处理的消息
*/
@Bean
public Queue deadLetterQueue() {
return new Queue("dead.letter.queue", true);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(dlxExchange())
.with("dead");
}
}
6.2 生产者:发送延迟消息
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class DelayMessageProducer {
private final RabbitTemplate rabbitTemplate;
public DelayMessageProducer(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
/**
* 发送统一延迟的消息(使用队列 TTL)
*/
public void sendWithQueueTtl(String payload) {
rabbitTemplate.convertAndSend("delay.exchange", "delay", payload);
log.info("[延迟队列] 消息已发送,将在队列 TTL 到期后被消费: {}", payload);
}
/**
* 发送自定义延迟的消息(使用消息级别 TTL)
* 注意:需要为每个不同的延迟时间创建独立队列,或使用延迟插件
*/
public void sendWithCustomDelay(String payload, long delayMillis) {
MessagePostProcessor processor = message -> {
message.getMessageProperties().setExpiration(String.valueOf(delayMillis));
return message;
};
rabbitTemplate.convertAndSend("delay.exchange", "delay", payload, processor);
log.info("[延迟队列] 消息已发送,延迟 {}ms: {}", delayMillis, payload);
}
}
6.3 消费者:带重试和死信处理
java
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
@Slf4j
public class ProcessConsumer {
private static final int MAX_RETRY = 3;
private final DelayMessageProducer delayProducer;
public ProcessConsumer(DelayMessageProducer delayProducer) {
this.delayProducer = delayProducer;
}
/**
* 处理队列消费者
* 手动 ACK 模式,可控制消息确认/拒绝
*/
@RabbitListener(queues = "process.queue", ackMode = "MANUAL")
public void handle(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String body = new String(message.getBody());
int retryCount = getRetryCount(message);
try {
log.info("[消费] 开始处理消息: {}, 重试次数: {}", body, retryCount);
// 模拟业务处理
boolean success = processBusinessLogic(body);
if (success) {
// 处理成功,确认消息
channel.basicAck(deliveryTag, false);
log.info("[消费] 处理成功: {}", body);
} else {
// 业务逻辑判定失败,根据重试次数决定
handleFailure(channel, deliveryTag, body, retryCount);
}
} catch (Exception e) {
log.error("[消费] 处理异常: {}", body, e);
handleFailure(channel, deliveryTag, body, retryCount);
}
}
private void handleFailure(Channel channel, long deliveryTag,
String body, int retryCount) throws IOException {
if (retryCount >= MAX_RETRY) {
// 超过最大重试次数,拒绝消息且不重新入队 → 进入死信队列
channel.basicReject(deliveryTag, false);
log.error("[消费] 超过最大重试次数({}), 消息进入死信队列: {}",
MAX_RETRY, body);
} else {
// 确认当前消息(从队列移除)
channel.basicAck(deliveryTag, false);
// 重新发送到延迟队列,指数退避
long delay = (long) Math.pow(3, retryCount + 1) * 1000;
log.warn("[消费] 处理失败, {}ms 后第 {} 次重试: {}",
delay, retryCount + 1, body);
// 注意:实际项目中应发送到独立的重试延迟队列
delayProducer.sendWithCustomDelay(
body + "|retry=" + (retryCount + 1), delay);
}
}
private int getRetryCount(Message message) {
// 从消息体或 header 中解析重试次数
String body = new String(message.getBody());
if (body.contains("|retry=")) {
String[] parts = body.split("\\|retry=");
return Integer.parseInt(parts[1]);
}
// 也可以从 x-death header 获取(死信路由会自动添加)
Map<String, Object> headers = message.getMessageProperties().getHeaders();
if (headers.containsKey("x-retry-count")) {
return (int) headers.get("x-retry-count");
}
return 0;
}
private boolean processBusinessLogic(String payload) {
// 实际业务逻辑
// 返回 true 表示成功,false 表示需要重试
return !payload.contains("error");
}
}
6.4 死信队列消费者:监控与告警
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
public class DeadLetterConsumer {
/**
* 死信队列消费者
* 用于监控、告警、记录失败消息
*/
@RabbitListener(queues = "dead.letter.queue")
public void handleDeadLetter(Message message) {
String body = new String(message.getBody());
Map<String, Object> headers = message.getMessageProperties().getHeaders();
// x-death 是 RabbitMQ 自动添加的死信元数据
List<Map<String, Object>> xDeath =
(List<Map<String, Object>>) headers.get("x-death");
String reason = "unknown";
String originQueue = "unknown";
if (xDeath != null && !xDeath.isEmpty()) {
Map<String, Object> death = xDeath.get(0);
reason = String.valueOf(death.get("reason"));
originQueue = String.valueOf(death.get("queue"));
}
log.error("[死信] 收到死信消息 | 来源队列: {} | 原因: {} | 内容: {}",
originQueue, reason, body);
// 可选操作:
// 1. 写入数据库记录
// saveToDeadLetterLog(originQueue, reason, body);
// 2. 发送告警通知(钉钉/企微/邮件)
// alertService.notify("死信消息告警", body);
// 3. 尝试转存到补偿表,由定时任务后续处理
// compensationService.save(body);
}
}
6.5 多级延迟队列(阶梯式重试)
java
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 多级延迟队列配置
* 实现阶梯式重试:3s → 10s → 30s → 死信
*/
@Configuration
public class MultiLevelDelayConfig {
// 第一级:延迟 3 秒
@Bean
public Queue delayLevel1Queue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 3000);
args.put("x-dead-letter-exchange", "retry.exchange");
args.put("x-dead-letter-routing-key", "retry");
return new Queue("delay.level1.queue", true, false, false, args);
}
// 第二级:延迟 10 秒
@Bean
public Queue delayLevel2Queue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 10000);
args.put("x-dead-letter-exchange", "retry.exchange");
args.put("x-dead-letter-routing-key", "retry");
return new Queue("delay.level2.queue", true, false, false, args);
}
// 第三级:延迟 30 秒
@Bean
public Queue delayLevel3Queue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 30000);
args.put("x-dead-letter-exchange", "retry.exchange");
args.put("x-dead-letter-routing-key", "retry");
return new Queue("delay.level3.queue", true, false, false, args);
}
@Bean
public DirectExchange retryExchange() {
return new DirectExchange("retry.exchange");
}
// 重试队列:所有延迟到期的消息最终到达这里被消费
@Bean
public Queue retryQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange");
args.put("x-dead-letter-routing-key", "dead");
return new Queue("retry.queue", true, false, false, args);
}
@Bean
public Binding retryBinding() {
return BindingBuilder.bind(retryQueue())
.to(retryExchange())
.with("retry");
}
}
使用时根据重试次数选择投递到哪个延迟队列:
java
public void retryWithLevel(String payload, int retryCount) {
String targetQueue;
switch (retryCount) {
case 1: targetQueue = "delay.level1.queue"; break;
case 2: targetQueue = "delay.level2.queue"; break;
case 3: targetQueue = "delay.level3.queue"; break;
default:
log.error("超过最大重试次数,放弃: {}", payload);
return;
}
rabbitTemplate.convertAndSend("", targetQueue, payload);
log.info("[重试] 第{}次重试,投递到: {}", retryCount, targetQueue);
}
七、延迟插件方案的代码示例
java
// 配置延迟交换机
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(
"delayed.exchange",
"x-delayed-message", // 插件提供的交换机类型
true, false, args
);
}
@Bean
public Queue targetQueue() {
return new Queue("target.queue", true);
}
@Bean
public Binding delayedBinding() {
return BindingBuilder.bind(targetQueue())
.to(delayedExchange())
.with("target")
.noargs();
}
// 发送时指定延迟
public void sendDelayed(String payload, long delayMillis) {
rabbitTemplate.convertAndSend("delayed.exchange", "target", payload, message -> {
// 插件识别这个 header
message.getMessageProperties().setHeader("x-delay", delayMillis);
return message;
});
}
八、x-death Header 详解
当消息进入死信队列时,RabbitMQ 自动在 header 中添加 x-death 数组,包含以下信息:
| 字段 | 含义 |
|---|---|
| queue | 消息死亡前所在的队列 |
| reason | 死亡原因:rejected / expired / maxlen |
| time | 死亡时间 |
| exchange | 消息原始交换机 |
| routing-keys | 原始路由键 |
| count | 该消息在该队列的死亡次数 |
可以利用 x-death 中的 count 做重试次数判断,比自定义 header 更可靠:
java
private int getDeathCount(Message message) {
Map<String, Object> headers = message.getMessageProperties().getHeaders();
List<Map<String, Object>> xDeath =
(List<Map<String, Object>>) headers.get("x-death");
if (xDeath != null && !xDeath.isEmpty()) {
return ((Long) xDeath.get(0).get("count")).intValue();
}
return 0;
}
九、总结对比
| 概念 | 核心作用 | 典型场景 |
|---|---|---|
| 延迟队列 | 消息延迟一段时间后再消费 | 订单超时关闭、预约提醒、延迟重试 |
| 死信队列 | 收集无法正常处理的消息 | 错误监控、告警、人工补偿 |
| TTL + 死信路由 | 用死信机制模拟延迟 | 不依赖插件的延迟队列实现 |
| 延迟插件 | 原生支持任意延迟 | 需要灵活延迟时间的场景 |
| 多级延迟 | 阶梯式退避重试 | 接口调用失败的递增重试 |
选型建议:
- 固定延迟时间、延迟种类有限 → TTL + 死信路由(无需插件)
- 每条消息延迟时间不同 → 延迟插件(需确认云服务是否支持)
- 消费失败兜底 → 死信队列(任何方案都应配置)