什么是 RabbitMQ 的死信队列?


面试考察点

  1. 概念理解:面试官想知道的不是你背不背得出来定义,而是你能不能把 "消息从正常消费到变成死信" 这条链路说清楚。

  2. 实战配置:Dead Letter Exchange(DLX)怎么配、x-dead-letter-routing-key 怎么设、绑定关系怎么建——这些写不出来说明没真用过。

  3. 架构设计意识:能不能结合业务场景聊。比如订单超时取消、消费失败告警,面试官想看的是你有没有从 "可靠性" 角度思考过消息系统。

核心答案

死信(Dead Letter)就是队列里没法被正常消费的消息。队列上如果配了死信交换机(DLX),消息一旦变成死信,RabbitMQ 会自动把它转发到 DLX,再由 DLX 路由到死信队列,等你后续处理。

消息变成死信有 3 种情况:

死信条件说明
被消费者拒绝(basic.reject / basic.nack)且 requeue = false消费端显式拒绝,不重新入队
TTL 过期消息在队列里待的时间超过了设定的 TTL
队列达到最大长度(x-max-length队列满了,最早进去的消息被挤掉

深度解析

死信队列的流转机制

正常的路是 Producer → Normal Queue → Consumer,消费成功就完事了。但如果出了问题——消息被拒了、过期了、或者队列满了装不下——这条消息就变成了死信。

Normal Queue 声明的时候可以通过 x-dead-letter-exchange 指定一个 DLX。消息一旦变成死信,RabbitMQ 自动把它投到这个 DLX。DLX 再根据 Routing Key(可以用 x-dead-letter-routing-key 单独指定)把消息路由到死信队列。

有个很多人搞混的点:DLX 不是什么特殊的 Exchange 类型。directtopicfanout 都行,它就是一个普通的 Exchange,只不过你拿它来接收死信而已。死信队列也是普通 Queue,没有 "死信队列" 这个类型。

代码怎么配

Spring Boot 项目里的完整配置,我贴一段:

// 正常业务交换机和队列
@Bean
public DirectExchange normalExchange() {
    return new DirectExchange("normal.exchange");
}

@Bean
public Queue normalQueue() {
    return QueueBuilder.durable("normal.queue")
            // 指定死信交换机
            .withArgument("x-dead-letter-exchange", "dlx.exchange")
            // 指定死信路由键(不设置则用原消息的 routing key)
            .withArgument("x-dead-letter-routing-key", "dlx.order")
            // 消息 TTL:30 秒未消费则过期
            .withArgument("x-message-ttl", 30000)
            .build();
}

@Bean
public Binding normalBinding() {
    return BindingBuilder.bind(normalQueue())
            .to(normalExchange())
            .with("order.create");
}

// 死信交换机和死信队列
@Bean
public DirectExchange dlxExchange() {
    return new DirectExchange("dlx.exchange");
}

@Bean
public Queue dlxQueue() {
    return QueueBuilder.durable("dlx.queue").build();
}

@Bean
public Binding dlxBinding() {
    return BindingBuilder.bind(dlxQueue())
            .to(dlxExchange())
            .with("dlx.order");
}

normal.queue 上挂了三个参数:死信交换机指向 dlx.exchange,路由键是 dlx.order,TTL 给了 30 秒。消息 30 秒没被消费,或者消费的时候被 nack 了,就会自动转到 dlx.queue。后面你给 dlx.queue 配个消费者,做告警、人工补偿、或者重新投递都行。

经典场景:订单超时取消

面试里出现频率最高的一个应用场景。

用户下单 30 分钟没付款,自动取消订单、释放库存。怎么做?

思路很直接:建一个 order.delay.queue,不挂任何消费者,TTL 设 30 分钟。消息到期后变成死信,经 DLX 路由到 order.cancel.queue,消费者拿到消息后查一下订单状态——没付款就取消,已付款就不管。

但这个方案有个坑。RabbitMQ 只会检查队头那条消息的过期时间,如果你的业务需要每条消息 TTL 不同(比如 A 商品 15 分钟超时,B 商品 30 分钟),排在后面的短 TTL 消息可能被队头的长 TTL 消息堵住,迟迟不触发过期。遇到这种情况,建议直接上 rabbitmq_delayed_message_exchange 插件,不存在队头阻塞的问题。

常见误区

  • 误区一:以为 DLX 是一种特殊的 Exchange 类型。其实不是,directtopicfanout 都可以作为 DLX,它只是被赋予了一个 "接收死信" 的角色。
  • 误区二:以为消息被消费失败就一定会进死信队列。错!必须是消费者显式 basic.nack / basic.reject 且设置 requeue = false 才行。如果你只是抛了个异常但没有手动 nack,消息可能会无限重试。
  • 误区三:把死信队列和延迟队列混为一谈。死信队列是 "兜底处理异常消息" 的机制,延迟队列只是利用了死信 + TTL 的特性来实现延迟效果,两者出发点不同。

面试高频追问

  1. 死信队列和延迟队列有什么区别? 死信队列处理的是 "消费不了的消息",属于异常处理。延迟队列是用 TTL + DLX 搭出来的延迟投递效果,属于业务设计。底层都用到了 DLX,但目的不一样。

  2. 消息消费失败后怎么让它进死信队列?channel.basicNack(deliveryTag, false, false),第三个参数 requeue = false 是关键,表示不重新入队。Spring AMQP 的话,抛一个 AmqpRejectAndDontRequeueException 效果一样。

  3. 延迟消息插件和死信方案怎么选? TTL 统一的简单场景,死信方案够用。每条消息延迟时间不同的话,上 rabbitmq_delayed_message_exchange 插件,省得踩队头阻塞的坑。

常见面试变体

  • "RabbitMQ 中消息在什么情况下会变成死信?"
  • "如何用 RabbitMQ 实现延迟队列?"
  • "RabbitMQ 如何保证消息的可靠消费?消费失败怎么办?"

记忆口诀

死信三条件:拒绝不重入(reject + requeue=false)、TTL 到期、队列满了挤出去。配置两参数:x-dead-letter-exchange + x-dead-letter-routing-key

总结

死信队列就是 RabbitMQ 的异常消息兜底。消息被拒绝、过期、或队列溢出时自动转到 DLX,再路由到死信队列等你处理。搞清楚 "什么时候变死信" 和 "DLX 怎么配" 这两点,面试基本稳了。生产环境里最常见的就是配合 TTL 做订单超时取消,但要注意队头阻塞问题,TTL 不统一的场景直接用延迟消息插件。