路由讲完了,这一篇讲消息在队列里的「生命周期管理」——什么时候该被删、什么时候该被转走、什么时候该过期。这套机制在 RabbitMQ 里由两个特性承载:死信(Dead Letter)和 TTL(Time To Live)

很多人把死信和 TTL 当成两个独立的小功能,配置时各配各的。但它们其实是同一件事的两面——都是消息离开当前队列的机制。死信是「因为某种原因被踢出队列」,TTL 是「因为时间到了被踢出队列」。理解了它们的本质,就能把它们组合出更高级的能力,比如延迟队列。

死信:消息的第二次生命

正常情况下,消息从队列被消费者 ack 后就消失了。但有几种情况,消息没有走「正常消费」这条路,而是变成了「死信」:

  • 消费者 reject 或 nack,且 requeue=false:消费者明确拒绝这条消息,且不让它重新入队,消息变成死信。
  • 消息 TTL 过期:消息在队列里待的时间超过了 TTL,还没被消费,变成死信。
  • 队列长度超限:队列达到了最大长度限制,新进来的消息会挤掉最老的消息,被挤掉的变成死信。

变成死信的消息,如果队列配置了 DLX(Dead Letter Exchange,死信交换器),会被转发到这个 DLX,再按 DLX 的路由规则投到死信队列。如果没有配置 DLX,死信就被直接丢弃。

死信机制的价值是让「处理不了的消息」有去处。没有死信,一条因为业务异常处理失败的消息,要么无限重试卡死消费者(毒丸),要么直接丢弃丢失数据。有了死信,这类消息进入死信队列,既不阻塞正常消费,也不丢失,可以后续人工介入、补偿重试、或者告警分析。

死信的完整链路

死信的工作方式,本质是「给队列挂一个出口」。配置上有三个参数:

  • x-dead-letter-exchange:死信转发到哪个 Exchange。
  • x-dead-letter-routing-key:转发时用什么 RoutingKey(可选,不设就用原消息的 RoutingKey)。
  • 队列本身的配置(在声明队列时设定,不能事后改)。

死信链路是:业务队列 →(消息变死信)→ DLX → 死信队列 → 死信消费者。DLX 可以是任意类型的 Exchange(direct/fanout/topic 都行),死信队列也是普通队列,只是用途不同。

一个完整的重试拓扑通常是这样的:

业务队列 (配 DLX=retry.exchange)
  ↓ 消费失败 nack(requeue=false)
retry.exchange (topic)
  ↓ 按重试次数路由
retry-1 队列 (TTL=10s, 配 DLX 回业务 exchange)
retry-2 队列 (TTL=60s, 配 DLX 回业务 exchange)
  ↓ 退避后重新投回业务队列

这套「业务队列 + 死信 + 延迟重试队列」的组合,是 RabbitMQ 实现可靠重试的标准模式。它不依赖额外的重试框架,纯靠队列配置就能实现指数退避重试。

TTL:消息的过期治理

TTL(Time To Live)是消息的「保质期」。超过 TTL 还没被消费的消息,会被自动删除(或转死信)。RabbitMQ 支持三个层级的 TTL:

  • 队列级 TTL:声明队列时设 x-message-ttl,队列里所有消息都有这个 TTL。
  • 消息级 TTL:发消息时设 expiration 属性,单条消息独立 TTL。
  • 队列本身 TTL:设 x-expires,整个队列多久没活动就自动删除(和消息 TTL 不同,这是队列级别的)。

TTL 最常见的用途是清理过期数据。比如验证码消息,5 分钟没消费就过期失效;或者临时任务队列,避免堆积太久占资源。

但 TTL 有一个反直觉的特性:队列级 TTL 是在消息投递给消费者时才检查的,不是实时的。也就是说,队头的消息如果过期了,但后面的消息没过期,broker 不会立刻删除队头的过期消息——它要等消息「走到队头即将被投递」时才检查 TTL。这意味着过期的消息可能还会占内存一段时间。这是 RabbitMQ 的设计取舍(实时检查 TTL 开销大),使用时要心里有数。

消息级 TTL 有一个更隐蔽的坑:消息级 TTL 的过期检查同样是在队头投递时,而且如果队列级 TTL 和消息级 TTL 同时存在,取较小的那个。如果消息级 TTL 没设好(比如设成不同值),消息可能不会按预期顺序过期——后进的消息可能因为 TTL 更小反而先过期,但因为不在队头而迟迟不被清理。

死信 + TTL = 延迟队列

死信和 TTL 单独用各有价值,组合起来能实现一个非常有用的能力——延迟队列

延迟队列的场景:消息发出后,不希望立即被消费,而是延迟一段时间。典型例子是「订单 30 分钟未支付自动取消」:下单时发一条消息,但希望它 30 分钟后才被消费(去检查订单状态,未支付就取消)。

RabbitMQ 没有原生的延迟队列类型,但可以用「TTL + 死信」组合:

  1. 建一个延迟队列,设 TTL(比如 30 分钟),配 DLX 指向业务 Exchange,但这个队列没有消费者
  2. 生产者把消息发到延迟队列,消息进入后无人消费,等 30 分钟 TTL 过期。
  3. TTL 过期,消息变成死信,被 DLX 转发到业务队列。
  4. 业务队列的消费者收到消息,执行「检查订单是否已支付」的逻辑。

消息从发出到被业务消费,正好延迟了 TTL 的时间。这就是用 RabbitMQ 原生能力实现延迟队列的经典模式。

TTL + 死信实现延迟队列

这种模式的局限:TTL 是队列级的,一个延迟队列只能对应一个固定延迟时长。如果业务需要「任意延迟时长」(比如有的延迟 10 分钟、有的延迟 1 小时),要么建多个不同 TTL 的延迟队列,要么用消息级 TTL——但前面说过消息级 TTL 有「不在队头不检查」的坑,会导致延迟不准。

RabbitMQ 还有一个更优雅的延迟方案:rabbitmq_delayed_message_exchange 插件。它提供一种特殊的 Exchange,发消息时直接指定延迟时长(x-delay 头),Exchange 会在指定时间后才路由消息。插件方案支持任意延迟时长,且没有队头检查的坑,是目前实现延迟队列的主流选择。代价是插件要单独安装维护。

几个设计边界

死信不是重试万能药。 死信转走的消息,如果不加控制地再投回业务队列,可能再次失败、再次死信,形成死循环。正确做法是给死信消息打上「重试次数」标记(用 headers),超过次数就转到「人工介入队列」不再重试。

TTL 要和死信配合。 单独设 TTL 而不配 DLX,过期消息就真的丢了,无法恢复。要保留过期消息用于分析,必须配 DLX 把过期消息转到归档队列。

队列参数不可变。 死信、TTL 这些队列参数(x-dead-letter-exchangex-message-ttl 等)在队列声明时就固定了,事后不能改。要改参数,只能删了队列重建(前提是队列空了或允许丢消息)。生产环境改这些参数要非常小心,通常需要停服、排空队列、重建。

收束:消息的生命周期是可设计的

死信和 TTL 让消息的生命周期变得可设计——消息不再是「进了队列就等消费」,而是可以被延迟、被过期、被重试、被归档。这套能力是 RabbitMQ 处理异步业务流程的利器,订单超时、重试退避、延迟任务这些常见需求都能用它原生实现。

下一篇继续讲队列的进阶形态——优先级、惰性、独占,看队列还能怎么变形以适应不同场景。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」

十三Tech公众号二维码