死信队列

DLQ(Dead Letter Queue),死信队列。当一个消息在队列中变成死信之后,它能被重新发送到 DLQ 中,与 DLQ 绑定的队列就是死信队列。

什么情况下消息会变成死信

  • 消息被拒绝:消费者 basic.nack/basic.reject 且 requeue=false
  • 消息过期:TTL 到期仍未被消费
  • 队列达到最大长度:x-max-length 超限后队头消息被挤出

以 RabbitMQ 为例:生产者把消息投递到普通队列,队列设置 TTL 为 10 秒;10 秒内无人消费,消息过期,若配置了 x-dead-letter-exchange,过期消息会被重新投递到死信交换机,最终落入死信队列等待人工或补偿程序处理。

死信队列的工程价值

死信队列不是”垃圾堆”,而是失败消息的隔离区与取证现场:

  • 隔离:失败消息不再阻塞正常队列的消费进度
  • 取证:消息体、失败次数、原始队列等头信息保留,便于排查
  • 补偿:运维或定时任务可对死信消息修复后重放(replay)

生产建议:死信队列必须配监控告警(堆积即报警),否则它只是把问题藏起来而不是解决问题。

延迟队列

延迟队列让消息在指定延迟后才可被消费,典型场景:订单 30 分钟未支付自动取消、会议开始前 10 分钟提醒。

RabbitMQ 本身不支持延迟队列,经典实现是 TTL + 死信 组合:

  • 建一个无消费者的”延迟队列”,设置消息 TTL = 延迟时间,并配置死信交换机指向”真实消费队列”
  • 消息在延迟队列中过期后,经死信路由进入真实队列,被消费者消费,等效于延迟投递

该方案有个著名陷阱:队列级 TTL 下,若消息延迟时间不同,队头一条长延迟消息会阻塞后面短延迟消息(过期检查只在队头进行)。解法是每条消息单独设 TTL 并接受其局限,或改用 rabbitmq_delayed_message_exchange 插件(基于 Erlang 定时器,按消息粒度触发)。

其他中间件的延迟方案对比:

  • RocketMQ:原生延迟消息,但只支持 18 个固定等级(1s~2h),任意精度需自研时间轮
  • Kafka:无原生延迟,通常用”时间戳 topic + 轮询搬迁”或外部调度器
  • Redis Zset:score=触发时间戳,消费者轮询 ZRANGEBYSCORE,轻量场景最简实现

重试队列

消费失败不等于消息失败:网络抖动、下游瞬时不可用都值得再试一次。重试队列的语义是:失败消息在延迟某个时间点后再投递给消费者,若持续失败达到上限次数,则转入死信队列等待人工干预。

设计要点:

  • 退避策略:重试间隔递增(1s、5s、30s、2m…),避免故障期间反复冲击下游
  • 次数上限:一般 3-5 次,超限进死信,防止无限重试放大故障
  • 幂等前提:重试意味着重复消费,消费端必须幂等(去重表/唯一键/状态机),否则重试比失败更危险
  • 区分错误类型:可恢复错误(超时、限流)才重试;不可恢复错误(参数非法、数据不存在)直接进死信,重试只是浪费

三者的协作关系

一次完整的失败处理链路通常是:消费失败 → 重试队列(退避重投)→ 超过上限 → 死信队列(隔离+告警)→ 人工/补偿修复后重放;而延迟队列复用死信机制实现定时语义。理解”死信是底座、延迟是用法、重试是策略”,这三种队列就不再是孤立概念,而是一套失败与定时处理的统一设施。