RabbitMQ 延迟队列与死信队列:原理、实现与实战

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 + 死信路由(无需插件)
  • 每条消息延迟时间不同 → 延迟插件(需确认云服务是否支持)
  • 消费失败兜底 → 死信队列(任何方案都应配置)
相关推荐
爱吃面的猫3 小时前
大数据Kafka3.x之——Kafka3.3.0安装与使用(详细)
大数据·分布式·zookeeper
小张同学a.11 小时前
zabbix企业级监控平台4——分布式监控与grafana数据可视化
linux·运维·数据库·分布式·信息可视化·zabbix·grafana
番茄炒鸡蛋加糖12 小时前
分布式 CAP、BASE 理论 & 中间件 CAP 取舍
分布式·中间件
海上小飞龙1 天前
Redis 分布式锁原理:从 SET NX EX 到 Redisson 看门狗
数据库·redis·分布式
明达智控技术1 天前
国产PLC、远程IO模块在食品包装机的应用
分布式·自动化
YOU OU1 天前
RabbitMQ应用问题
分布式·rabbitmq
YOU OU1 天前
RabbitMQ高级特性
分布式·rabbitmq
珠***格2 天前
双碳目标下:四可装置如何助力光伏消纳与碳数据上报
网络·人工智能·分布式·安全·边缘计算
科力锐品牌君2 天前
行业龙头|科力锐全链路防勒索 + 多中心容灾方案,构筑河南翔宇医疗业务安全闭环!
网络·数据库·分布式·安全·数据安全·备份