六、死信队列

本文详细讲解了如何在RabbitMQ中实现死信队列,包括死信概念、常见来源(如消息TTL过期、队列长度限制和消息被拒),并通过代码实例展示了如何配置交换机、队列和消费者来处理死信消息,确保业务数据完整性。

RabbitMQ目录



死信队列

1、死信的概念

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。

2、死信的来源

  • 消息 TTL(存活时间) 过期

  • 队列达到最大长度(队列满了,无法再添加数据到队列中)

  • 消息被拒绝(消费方拒绝应答:basic.reject 或 basic.nack)并且(不放回队列中: requeue=false)

3、死信实战

3.1. 代码架构图

描述:一个生产者,两个消费者,有一个正常的交换机,类型为direct,路由模式。一个消息正常发给消费者C1,结果出现了上面描述的三种情况:消息 TTL 过期、队列达到最大长度、消息被拒绝,消息成为死信,之后消息被转发到(死信交换机)ded_exchange,类型也为direct,之后进入死信队列,最终由消费者C2消费。

代码核心部分在C1。

3.2. 消息TTL 过期

先启动C1,让其创建正常交换机、死信交换机、正常队列、死信队列,以及之间的绑定关系,之后关闭C1,生产者发送消息,这是模拟“消息TTL 过期”。之后启动C2去消息死信队列中的消息。

消费者 C1 代码 (启动之后关闭该消费者 模拟其接收不到消息)

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 死信队列 消费者C1
 */
public class Consumer01 {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明死信队列,并绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");

        //正常队列 将消息转发给死信队列 所需参数
        Map<String, Object> params = new HashMap<>();
        //正常队列 设置 死信交换机,参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key,参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");

        //声明正常队列,并绑定正常交换机
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");

        System.out.println("Consumer01等待接收消息........... ");
        //成功接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer01 接收到消息"+message);
        };
        //接收消息
        channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {});
    }
}

生产者代码:

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 生产者
 */
public class Producer {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);

        //死信消息,设置消息的 TTL(存活时间) 时间:10s
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

        for (int i = 1; i <11 ; i++) {
            String message="info"+i;
            //正常交换机名称、正常交换机于正常队列之间的routingKey、配置参数、消息
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
            System.out.println("生产者发送消息:"+message);
        }

    }
}

消费者 C2 代码(以上步骤完成后 启动 C2 消费者 它消费死信队列里面的消息)

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 消费者C2 - 消费死信消息
 */
public class Consumer02 {

    //死信队列的名称 - 和C1中保持一致
    private static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("Consumer02等待接收死信队列消息........... ");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, consumerTag -> {});
    }
}

3.3. 队列达到最大长度

在测试时,还是需要关闭C1,这样消息才会积压,正常队列才会满。

1.消息生产者代码去掉 TTL 属性:

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 生产者
 */
public class Producer {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);

        //该信息是用作演示队列个数限制
        for (int i = 1; i <11 ; i++) {
            String message="info"+i;
            //正常交换机名称、正常交换机于正常队列之间的routingKey、配置参数、消息
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
            System.out.println("生产者发送消息:"+message);
        }

    }
}

2.C1 消费者修改以下代码

注意此时需要把原先队列删除 因为参数改变了

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 死信队列 消费者C1
 *
 * @auther wlw
 * @date 2022/3/13
 */
public class Consumer01 {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明死信队列,并绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");

        //正常队列 将消息转发给死信队列 所需参数
        Map<String, Object> params = new HashMap<>();
        //正常队列 设置 死信交换机,参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key,参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");
        //设置正常队列的限制
        params.put("x-max-length", 6);

        //声明正常队列,并绑定正常交换机
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");

        System.out.println("Consumer01等待接收消息........... ");
        //成功接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer01 接收到消息"+message);
        };
        //接收消息
        channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {});
    }
}

3.C2 消费者代码不变(启动 C2 消费者)

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 消费者C2 - 消费死信消息
 */
public class Consumer02 {

    //死信队列的名称 - 和C1中保持一致
    private static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("Consumer02等待接收死信队列消息........... ");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, consumerTag -> {});
    }
}

3.4. 消息被拒

1.消息生产者代码同上生产者一致

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 生产者
 */
public class Producer {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);

        for (int i = 1; i <11 ; i++) {
            String message="info"+i;
            //正常交换机名称、正常交换机于正常队列之间的routingKey、配置参数、消息
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
            System.out.println("生产者发送消息:"+message);
        }

    }
}

2.C1 消费者代码

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 死信队列 消费者C1
 */
public class Consumer01 {

    //正常交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明正常交换机和死信交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明死信队列,并绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");

        //正常队列 将消息转发给死信队列 所需参数
        Map<String, Object> params = new HashMap<>();
        //正常队列 设置 死信交换机,参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key,参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");
        //设置正常队列的限制
        //params.put("x-max-length", 6);

        //声明正常队列,并绑定正常交换机
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");

        System.out.println("Consumer01等待接收消息........... ");
        //成功接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            if(message.equals("info5")){
                // "info5"为消息内容
                System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
                //拒绝应答,requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                //并且下面接收消息也要设置为手动应答
                channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
            }else {
                System.out.println("Consumer01 接收到消息"+message);
                //确认应答
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        //接收消息
        boolean autoAck = false; //表示手动应答
        channel.basicConsume(normalQueue, autoAck, deliverCallback, consumerTag -> {});
    }
}

3.C2 消费者代码不变

package com.wlw.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wlw.rabbitmq.utils.RabbitMqUtils;

/**
 * 死信队列 消费者C2 - 消费死信消息
 */
public class Consumer02 {

    //死信队列的名称 - 和C1中保持一致
    private static final String DEAD_QUEUE = "dead-queue";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("Consumer02等待接收死信队列消息........... ");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(DEAD_QUEUE, true, deliverCallback, consumerTag -> {});
    }
}

启动消费者 1 然后再启动消费者 2

<think>好的,我现在需要解释MQ中的死信队列。首先,用户可能已经了解过Feign和MQ的区别,现在想深入理解死信队列的概念。用户可能是开发人员或者系统架构师,需要在实际项目中使用或优化消息队列。 首先,我应该回忆死信队列的基本定义。死信队列(Dead Letter Queue,DLQ)是用于存储无法被正常消费的消息的队列。当消息在特定条件下无法被处理时,会被转移到DLQ中,以便后续处理或分析。 接下来,需要确定触发消息进入死信队列的条件。根据之前的知识,常见的条件包括消息被消费者明确拒绝、消息过期(TTL)、队列达到最大长度等。需要逐一列出这些条件,并给出具体例子,比如消息重试次数超过限制。 然后,解释死信队列的作用。用户可能想知道为什么需要这个机制,比如处理异常情况、避免消息丢失、方便问题排查等。要强调DLQ在系统健壮性和可维护性中的重要性。 可能用户还会关心如何配置死信队列,比如在RabbitMQ或Kafka中的具体实现方式。但用户的问题主要是解释概念,所以需要保持简洁,但可以提到常见MQ系统的处理方式。 需要举一个实际的应用场景,比如订单超时未支付的处理,用死信队列来实现延迟重试或记录日志。这样用户能更好地理解其实际用途。 另外,需要区分死信队列和其他机制,比如重试队列。死信队列是最终处理无法消费的消息,而重试队列可能还会尝试重新投递。这一点要说明清楚,避免混淆。 还要注意结构清晰,分点列出触发条件、核心作用、应用场景和配置示例。可能使用代码片段或配置示例来增强理解,比如RabbitMQ的配置参数。 最后,总结死信队列的重要性,并提醒用户在处理消息时合理配置DLQ,以提高系统的可靠性和可维护性。确保语言通俗易懂,避免过于技术化的术语,但又要准确无误。</think>--- ### 死信队列(Dead Letter Queue, DLQ)核心解析 **一句话定义**: 死信队列是消息队列中专门存储 **"处理失败消息"** 的隔离队列,相当于消息系统的 **"急诊室"**,用于隔离、诊断和处理异常消息。 --- ### 一、消息进入死信队列的 **3大触发条件** | 触发条件 | 典型场景示例 | 处理逻辑 | |-------------------------|----------------------------------|-----------------------------------| | **消息被明确拒绝** | 消费者手动发送`basic.reject`或`basic.nack`且不重新入队 | 消息立即进入DLQ | | **消息存活超时(TTL)** | 消息设置10秒有效期,超时未被消费 | 到期后自动转入DLQ | | **队列达到最大限制** | 队列设置最大长度1000,新消息超出限制 | 最旧的消息被挤出队列进入DLQ | --- ### 二、死信队列的 **4个核心作用** 1. **故障隔离** - 将异常消息与正常业务流量分离,避免"毒药消息"(反复失败的消息)阻塞正常处理 - 示例:错误格式的消息会持续导致消费者崩溃,隔离后不影响其他消息处理 2. **重试管理** - 通过TTL+DLQ实现 **延迟重试机制**(比直接重试更可控) - 典型方案:首次失败 → 进入DLQ → 设置5分钟延迟 → 重新投递回业务队列 3. **审计追踪** - 保留所有失败消息,便于事后分析系统问题 - 可通过DLQ中的消息反推业务逻辑缺陷或数据异常 4. **最终兜底** - 防止消息永久丢失,为人工干预提供最后机会 - 运维人员可手动从DLQ取出消息进行补偿处理 --- ### 三、经典应用场景 #### 案例1:支付订单超时关闭 ```plaintext 正常队列:order_pay DLQ:order_pay_dlq 1. 用户下单生成消息(TTL=15分钟) 2. 若15分钟内未支付,消息自动进入DLQ 3. 监听DLQ的服务触发订单关闭操作 ``` #### 案例2:消息格式升级兼容 ```plaintext 旧版本服务无法解析新格式消息 → 消息进入DLQ 运维人员将DLQ消息转换格式后重新投递 ``` --- ### 四、RabbitMQ配置示例 ```java // 声明死信交换机和队列 @Bean public DirectExchange dlxExchange() { return new DirectExchange("dlx.exchange"); } @Bean public Queue dlxQueue() { return new Queue("dlx.queue"); } @Bean public Binding dlxBinding() { return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.routingKey"); } // 业务队列绑定死信配置 @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", "dlx.routingKey"); // 指定路由键 args.put("x-message-ttl", 60000); // 消息存活时间60秒 args.put("x-max-length", 1000); // 队列最大容量1000条 return new Queue("business.queue", true, false, false, args); } ``` --- ### 五、最佳实践建议 1. **监控告警** - 对DLQ设置消息数量阈值告警(如1小时内积累超过100条即报警) 2. **TTL分层** ```plaintext 首次失败 → DLQ1(延迟1分钟重试) 二次失败 → DLQ2(延迟10分钟重试) 三次失败 → 人工处理队列 ``` 3. **消息染色** - 在消息头添加`retry-count`字段记录重试次数 4. **清理策略** - 设置DLQ消息保留时间(如最多保存7天),避免磁盘爆满 --- ### 、与重试队列的区别 | | **死信队列** | **重试队列** | |------------------|----------------------------------|----------------------------------| | 定位 | 最终兜底机制 | 临时缓冲机制 | | 消息处理 | 需要人工介入或特殊处理 | 系统自动重新投递 | | 典型实现 | RabbitMQ的DLX机制 | Kafka消费者自行维护重试逻辑 | | 消息生命周期 | 可能永久留存 | 达到最大重试次数后进入DLQ | 通过合理使用死信队列,可使消息系统具备 **自我修复能力** 和 **故障容忍度**,是构建可靠分布式系统的必备机制。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值