什么是 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 过期、队列超长丢弃。
- 死信必须有人消费、有监控,否则只是把丢失换了个地方。
- 死信队列自身也要配持久化与容量,且不能形成环形投递。