Appearance
毒消息、重试与 DLQ(routing / retry-dlq)
本页结论:Topic Exchange 按路由键把同一事件分发给不同订阅者;毒消息经「TTL + DLX 回环」有限重试 3 次后被隔离进 DLQ,正常消息不受阻塞——RabbitMQ 的重试是组合配置出来的模式,不是 Broker 内置的消费重试。
routing:Topic Exchange 分发
拓扑
三条消息、三种路由键:order.created、order.created、order.created.eu。绑定决定分发:
| 队列 | 绑定模式 | 收到的消息 | 说明 |
|---|---|---|---|
| orders.routing.created | order.created | 2 | 精确匹配,order.created.eu 不是 order.created |
| orders.routing.all | order.# | 3 | # 匹配零个或多个段 |
| orders.routing.eu | order.created.eu | 1 | 精确匹配 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 |
断言
| confirmed | 3 |
| received:orders.routing.created | 2 |
| unique:orders.routing.created | 2 |
| depthAfter:orders.routing.created | 0 |
| received:orders.routing.all | 3 |
| unique:orders.routing.all | 3 |
| depthAfter:orders.routing.all | 0 |
| received:orders.routing.eu | 1 |
| unique:orders.routing.eu | 1 |
| depthAfter:orders.routing.eu | 0 |
归一化日志
[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 routingretry-dlq:毒消息的有限重试
为什么要隔离毒消息
一条无法被业务处理的消息(Schema 缺失字段、引用不存在的数据、确定性报错的代码路径)如果被无限 basicNack(requeue=true),会卡住队列头部,阻塞后续所有消息。正确做法是:有限重试 + 隔离到死信队列(Dead Letter Queue, DLQ),保留证据供人工处理。
拓扑:TTL + DLX 回环
RabbitMQ 没有内置「消费失败自动重试 N 次」。本实验用队列参数组合出重试环:
orders.work:x-dead-letter-exchange=""+x-dead-letter-routing-key=orders.retry,被拒绝的消息进入重试队列。orders.retry:x-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-dlqProducer 发送 order-1001、order-1002 与一条故意不符合 Schema 的毒消息(fixture poison-message.json,payload 缺少必填字段,业务写入必然抛异常)。Consumer:
- order-1001/1002:
business_committed,正常落库(business_rows=2)。 - 毒消息:attempt=1 失败 →
status=retry;约 1 秒后 attempt=2 再失败;attempt=3 达到上限 →status=poison_to_dlq。 - 断言 DLQ 深度为 1,work/retry 队列清空。
断言
| 断言 | 期望 | 说明 |
|---|---|---|
| confirmed | 3 | 含毒消息,Broker 不校验业务内容 |
| business_rows | 2 | 毒消息未产生业务写入 |
| poisonAttempts | 1,2,3 | x-death 计数确实递增 |
| poisonMovedToDlq | 1 | 显式投递 DLQ 恰好一次 |
| dlqMessages | 1 | DLQ 隔离成功 |
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 |
断言
| confirmed | 3 |
| business_rows | 2 |
| poisonAttempts | [ 1, 2, 3 ] |
| poisonMovedToDlq | 1 |
| dlqMessages | 1 |
| workQueueDepthAfter | 0 |
| retryQueueDepthAfter | 0 |
| consumerExitCode | 0 |
归一化日志
[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。
官方资料与版本说明
- RabbitMQ 4.1.4,
amqp-client5.34.0。 - Dead Letter Exchanges:https://www.rabbitmq.com/docs/dlx(checkedAt: 2026-08-19)
- Time-to-Live:https://www.rabbitmq.com/docs/ttl(checkedAt: 2026-08-19)
- Topic Exchange:https://www.rabbitmq.com/tutorials/amqp-concepts#exchange-topic(checkedAt: 2026-08-19)