什么是 RabbitMQ 的死信队列?

#中间件#面试八股

什么是 RabbitMQ 的死信队列?

结论:死信队列(Dead Letter Queue,DLQ)不是一种特殊队列,而是"给普通队列配一个死信交换机(DLX)"之后,那些无法被正常消费的消息被重新路由到的队列。消息变成死信只有三种原因:被拒绝且不重回队列、TTL 过期、队列超长被丢弃。死信的价值在于把"失败消息"从静默丢失变成可观测、可补偿的数据。

一、死信的流转

                    ┌─────────────────────────────┐
 basicNack(requeue  │  业务队列 order.pay.q        │
   =false)  ───────>│  x-dead-letter-exchange     │
 TTL 过期   ───────>│  = "order.dlx"              │
 超长被丢弃 ───────>│  x-dead-letter-routing-key  │
                    │  = "order.pay.dead"         │
                    └──────────────┬──────────────┘
                                   │ 重新发布到 DLX
                                   ▼
                    ┌─────────────────────────────┐
                    │  DLX: order.dlx (direct)     │
                    └──────────────┬──────────────┘
                                   │ 按 routing key 绑定
                                   ▼
                    ┌─────────────────────────────┐
                    │  死信队列 order.pay.dlq       │──> 补偿程序 / 人工
                    └─────────────────────────────┘

关键点:死信是"重新发布"到 DLX,而不是原队列内部转移。因此死信消息会带上原始交换机和路由键信息(x-death 头),但消息 ID 会保留、投递计数会重置。

二、三种触发条件

触发条件说明常见误用
被拒绝且不重回队列basicReject(tag, false) 或 basicNack(tag, false, false)用 requeue=true 死循环重投,消息永远不进死信
消息 TTL 过期队列级 x-message-ttl 或消息级 expiration以为过期消息会保留,实际直接消失或进死信
队列达到最大长度x-max-length + x-overflow=drop-head 时丢弃队头丢弃的正是最老、最该被处理的消息

还有一种容易忽略的情况:队列被删除(例如误删 vhost 或队列),消息不会进死信,而是直接消失。

三、声明死信队列

// 1. 死信交换机与死信队列
channel.exchangeDeclare("order.dlx", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("order.pay.dlq", true, false, false,
        Map.of("x-queue-type", "quorum"));
channel.queueBind("order.pay.dlq", "order.dlx", "order.pay.dead");

// 2. 业务队列绑定死信参数
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "order.dlx");
args.put("x-dead-letter-routing-key", "order.pay.dead");
args.put("x-message-ttl", 60_000);
args.put("x-max-length", 500_000);
channel.queueDeclare("order.pay.q", true, false, false, args);
// 3. 消费失败时显式拒绝,让消息进死信
channel.basicConsume("order.pay.q", false, (tag, msg) -> {
    try {
        handler.handle(msg);
        channel.basicAck(tag, false);
    } catch (RecoverableException e) {
        channel.basicNack(tag, false, true);    // 可恢复:重回队列重试
    } catch (Exception e) {
        channel.basicNack(tag, false, false);   // 不可恢复:进死信
    }
});

四、死信队列的使用姿势

做法说明
独立消费死信补偿程序读 DLQ,按业务做重放、人工审核或归档
监控告警DLQ 有消息即告警,它是"业务失败率"的直接指标
保留期限死信队列也受 TTL 与长度限制,需单独配置,避免死信被再次丢弃
避免环形死信死信队列不要再配同一个 DLX,否则消息会在两个队列间无限循环

五、常见追问

  • 死信消息能回到原队列吗? 可以,但必须由补偿程序显式重新发布,并带上重试次数字段;无脑回投会形成"失败—死信—回投—再失败"的循环。
  • 消费端一直 requeue 会进死信吗? 不会。requeue=true 表示重新入原队列,只有 requeue=false 或 TTL 过期、超长才会进死信。
  • 死信队列要不要设为持久化与仲裁队列? 要。死信往往是核心失败数据,丢失意味着无法对账,配置应与业务队列一致。
  • 死信和延迟队列什么关系? 延迟队列常用"TTL 过期转死信"实现,此时死信队列承担的是"到期投递"职责;但死信队列的语义更广,还包括失败与溢出。

小结

  • 死信队列 = 普通队列 + x-dead-letter-exchange,本质是失败消息的再路由。
  • 三种触发条件:拒绝且不重回队列、TTL 过期、队列超长丢弃。
  • 死信必须有人消费、有监控,否则只是把丢失换了个地方。
  • 死信队列自身也要配持久化与容量,且不能形成环形投递。
笔记加载中…