Skip to content

毒消息、重试与 DLQ(routing / retry-dlq)

本页结论:Topic Exchange 按路由键把同一事件分发给不同订阅者;毒消息经「TTL + DLX 回环」有限重试 3 次后被隔离进 DLQ,正常消息不受阻塞——RabbitMQ 的重试是组合配置出来的模式,不是 Broker 内置的消费重试。

routing:Topic Exchange 分发

拓扑

三条消息、三种路由键:order.createdorder.createdorder.created.eu。绑定决定分发:

队列绑定模式收到的消息说明
orders.routing.createdorder.created2精确匹配,order.created.eu 不是 order.created
orders.routing.allorder.#3# 匹配零个或多个段
orders.routing.euorder.created.eu1精确匹配 EU 变体

运行与断言

bash
npm run lab -- rabbitmq routing

断言按队列核对收到数量、唯一 messageId 数量与消费后的队列深度。要点:同一条消息被复制到多个队列是发布订阅(Pub/Sub)语义;队列内部才是竞争消费。

verifiedrabbitmq / routingbroker 4.1.4 · java-amqp-client-5.34.0
镜像rabbitmq:4.1.4-management@sha256:294b01e1796a8acede4619f32a1c394fae1f8021e57986ea01aad38dc2a4f502
捕获时间2026-08-19T07:59:13.973Z
耗时 / 退出码17233 ms / exit 0
断言
confirmed3
received:orders.routing.created2
unique:orders.routing.created2
depthAfter:orders.routing.created0
received:orders.routing.all3
unique:orders.routing.all3
depthAfter:orders.routing.all0
received:orders.routing.eu1
unique:orders.routing.eu1
depthAfter:orders.routing.eu0
归一化日志
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.events routingKey=order.created durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.events routingKey=order.created durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-1003 traceId=trace-3 correlationId=order-1003 destination=orders.events routingKey=order.created.eu durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing destination=orders.events confirmed=3 status=done
[assert] confirmed=3 PASS
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.routing.created consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.routing.created attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.routing.created consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.routing.created attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing queue=orders.routing.created received=2 status=done
[assert] received:orders.routing.created=2 PASS
[assert] unique:orders.routing.created=2 PASS
[assert] depthAfter:orders.routing.created=0 PASS
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.routing.all consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.routing.all attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.routing.all consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.routing.all attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-1003 traceId=trace-3 correlationId=order-1003 destination=orders.routing.all consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-1003 traceId=trace-3 correlationId=order-1003 destination=orders.routing.all attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing queue=orders.routing.all received=3 status=done
[assert] received:orders.routing.all=3 PASS
[assert] unique:orders.routing.all=3 PASS
[assert] depthAfter:orders.routing.all=0 PASS
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-1003 traceId=trace-3 correlationId=order-1003 destination=orders.routing.eu consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-1003 traceId=trace-3 correlationId=order-1003 destination=orders.routing.eu attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=routing queue=orders.routing.eu received=1 status=done
[assert] received:orders.routing.eu=1 PASS
[assert] unique:orders.routing.eu=1 PASS
[assert] depthAfter:orders.routing.eu=0 PASS
npm run lab -- rabbitmq routing

retry-dlq:毒消息的有限重试

为什么要隔离毒消息

一条无法被业务处理的消息(Schema 缺失字段、引用不存在的数据、确定性报错的代码路径)如果被无限 basicNack(requeue=true),会卡住队列头部,阻塞后续所有消息。正确做法是:有限重试 + 隔离到死信队列(Dead Letter Queue, DLQ),保留证据供人工处理。

拓扑:TTL + DLX 回环

RabbitMQ 没有内置「消费失败自动重试 N 次」。本实验用队列参数组合出重试环:

  • orders.workx-dead-letter-exchange="" + x-dead-letter-routing-key=orders.retry,被拒绝的消息进入重试队列。
  • orders.retryx-message-ttl=1000 + DLX 指回 orders.work,到期后自动回到工作队列,形成带 1 秒延迟的重试。
  • 重试次数来自消息头 x-death:Broker 在消息每次被 dead-letter 时追加记录,消费者统计 reason=rejected 的累计计数,attempt = 1 + rejected 计数
  • 达到 max-attempts=3 后,消费者显式发布到 orders.dlq 并 ACK 原消息。

实验过程

bash
npm run lab -- rabbitmq retry-dlq

Producer 发送 order-1001、order-1002 与一条故意不符合 Schema 的毒消息(fixture poison-message.json,payload 缺少必填字段,业务写入必然抛异常)。Consumer:

  1. order-1001/1002:business_committed,正常落库(business_rows=2)。
  2. 毒消息:attempt=1 失败 → status=retry;约 1 秒后 attempt=2 再失败;attempt=3 达到上限 → status=poison_to_dlq
  3. 断言 DLQ 深度为 1,work/retry 队列清空。

断言

断言期望说明
confirmed3含毒消息,Broker 不校验业务内容
business_rows2毒消息未产生业务写入
poisonAttempts1,2,3x-death 计数确实递增
poisonMovedToDlq1显式投递 DLQ 恰好一次
dlqMessages1DLQ 隔离成功
verifiedrabbitmq / retry-dlqbroker 4.1.4 · java-amqp-client-5.34.0
镜像rabbitmq:4.1.4-management@sha256:294b01e1796a8acede4619f32a1c394fae1f8021e57986ea01aad38dc2a4f502
捕获时间2026-08-19T07:59:43.059Z
耗时 / 退出码17211 ms / exit 0
断言
confirmed3
business_rows2
poisonAttempts[ 1, 2, 3 ]
poisonMovedToDlq1
dlqMessages1
workQueueDepthAfter0
retryQueueDepthAfter0
consumerExitCode0
归一化日志
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.work routingKey=orders.work durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.work routingKey=orders.work durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work routingKey=orders.work durationMs=<ms> status=confirmed
[producer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq destination=orders.work confirmed=3 status=done
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.work consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-1 eventType=order.created schemaVersion=1 aggregateId=order-1001 traceId=trace-1 correlationId=order-1001 destination=orders.work attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.work consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-2 eventType=order.created schemaVersion=1 aggregateId=order-1002 traceId=trace-2 correlationId=order-1002 destination=orders.work attempt=1 status=business_committed
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work consumer=consumer-1 attempt=1 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work attempt=1 status=retry
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work consumer=consumer-1 attempt=2 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work attempt=2 status=retry
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work consumer=consumer-1 attempt=3 redelivered=false status=received
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq messageId=mid-3 eventType=order.created schemaVersion=1 aggregateId=order-poison traceId=trace-3 correlationId=order-poison destination=orders.work attempt=3 status=poison_to_dlq
[consumer] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq queue=orders.work received=5 status=done
[inspect] timestamp=<ts> level=INFO service=order-service product=rabbitmq lab=retry-dlq business_rows=2 processed_rows=2 status=snapshot
[assert] confirmed=3 PASS
[assert] business_rows=2 PASS
[assert] poisonAttempts=1,2,3 PASS
[assert] poisonMovedToDlq=1 PASS
[assert] dlqMessages=1 PASS
[assert] workQueueDepthAfter=0 PASS
[assert] retryQueueDepthAfter=0 PASS
[assert] consumerExitCode=0 PASS
npm run lab -- rabbitmq retry-dlq

保证成立的条件 / 不保证什么

  • 重试延迟由 retry 队列 TTL 决定;TTL 到期是从队头开始计算的(队列级 TTL),不适合做大量差异化延迟的调度器。
  • 进入 DLQ 不代表消息「处理失败的原因」被记录;生产实践应同时把失败原因写入日志或旁路存储,DLQ 只保留原始消息。
  • DLX、TTL、x-death 都是组合使用的队列特性,与 Kafka 的 retry topic 模式、RocketMQ 的 Broker 内置重试不是同一机制,不可互相类比(见 投递语义矩阵 与后续横向矩阵)。
  • 毒消息进 DLQ 后业务侧仍需告警与人工回放;DLQ 不是「删掉就没事」的垃圾桶。

常见误区

  • 「nack(requeue=true) 一直重试」——无延迟、无计数,毒消息会无限占用队头。
  • 「RabbitMQ 有内置消费重试」——重试环是应用 + 队列参数组合出来的模式。
  • 「DLQ 里的消息会自动处理」——不会,需要人工或独立流程消费 DLQ。

官方资料与版本说明

以统一实验验证消息系统语义边界