bigywater.tech
RabbitMQ · 10

死信队列:TTL、拒绝与队列长度触发

死信队列是 RabbitMQ 里处理失败消息、做延迟和重试的基础。消息什么时候会死、死的时候带着什么信息、死信交换机配错了会怎样、死信能不能转圈?这篇一项项在 RabbitMQ 4.3.6 上验证。

3 个原因
rejected(被拒绝)、expired(过期)、maxlen(队列超长),都写在 x-death 的 reason 里
count 0→3
重试环里消息每绕一圈,x-death 中 main 的 count 加 1
(0, 0)
DLX 无绑定或不存在时,死信的去向:消息直接消失,没有报错

什么时候死

实测 · 本机执行输出
  死信队列收到: body='bad-msg',exchange='dlx',routing_key='m1'
  x-death = [{count: 1, reason: 'rejected', queue: 'm1', exchange: '', routing-keys: ['m1']}]
  其它头: {'k': 'v', 'x-first-death-exchange': '', 'x-first-death-queue': 'm1', 'x-first-death-reason': 'rejected', 'x-last-death-exchange': '', 'x-last-death-queue': 'm1', 'x-last-death-reason': 'rejected'}
  x-death[0] 的全部键: ['count', 'exchange', 'queue', 'reason', 'routing-keys', 'time']

消息成为死信有三种常见原因:被 reject / nack 且 requeue=false(rejected)、TTL 到期(expired)、队列超过 x-max-length 被挤出(maxlen)。队列声明时设置了 x-dead-letter-exchange,死信才会被重新发布到那个 exchange,否则就是丢弃。

x-death 头

实测 · 本机执行输出
  'queue-ttl': x-death = [{count: 1, reason: 'expired', queue: 'm2', exchange: '', routing-keys: ['m2']}]
  'msg-ttl': x-death = [{count: 1, reason: 'expired', queue: 'm2b', exchange: '', routing-keys: ['m2b']}]

死信会带上 x-death 数组,每项记录某队列因某原因死亡的 count、queue、exchange、routing-keys 和 time;另有 x-first-death-* 与 x-last-death-*。原来的 body 和自定义头原样保留。

队列超长

实测 · 本机执行输出
  主队列里剩: ['msg3', 'msg4', 'msg5']
  死信队列收到: [('msg0', 'maxlen'), ('msg1', 'maxlen'), ('msg2', 'maxlen')]

设置 x-max-length 且溢出策略是默认的 drop-head 时,队头(最老的)被挤出,配了死信交换机就变成 reason=maxlen 的死信,而不是被丢弃。

routing key

实测 · 本机执行输出
  dlq.a(绑定 key=orig) 收到: [('to-both', 'm4a', ['orig'])]
  dlq.b(绑定 key=fixed) 收到: [('to-both', 'm4b', ['orig'])]
  m4a 没设 x-dead-letter-routing-key:死信带原来的 key orig,进了 dlq.a;m4b 设成 fixed:进了 dlq.b,x-death 里记的 routing-keys 仍是原来的 orig

死信的 routing key 默认沿用原来的,可以用 x-dead-letter-routing-key 改写。死信交换机就是普通 exchange,有没有队列收到完全取决于它的绑定。

配错了不会报错

实测 · 本机执行输出
  拒绝了 3 条,m5 里剩 (0, 0),没有绑定的 DLX 没有留下任何东西(消息消失)
  DLX 指向一个不存在的 exchange:拒绝后 m5 里剩 (0, 0),channel 仍然 打开,消息消失

DLX 没有匹配的绑定,或者根本不存在,死信就直接消失,既不报错也不影响 channel。

重试

实测 · 本机执行输出
  每次拿到消息的 (距开始 ms, x-death 里 main 的 count, redelivered): [(0, 0, False), (319, 1, True), (628, 2, True), (962, 3, True)]
  第 5 次拿到时的 x-death: x-death = [{count: 4, reason: 'expired', queue: 'wait', exchange: 'retry.dlx', routing-keys: ['wait']}, {count: 4, reason: 'rejected', queue: 'main', exchange: '', routing-keys: ['main']}]
  first-death: queue='main' reason='rejected' exchange=''; last-death: queue='wait' reason='expired'

main 的死信交换机指向一个带 TTL 的 wait 队列,wait 的死信再回到 main,就组成了“失败 → 等一会 → 重试”的环;x-death 里的 count 可以用来限制重试次数。

死信环

实测 · 本机执行输出
  1.5 秒后 loopq 里剩 0 条(TTL 200 ms,如果没有循环检测它会一直转下去)
  broker 日志里关于这个循环的行:
    <0.1085.0> Message dropped because the following list of queues (ordered by death recency) contains a dead letter cycle without reason 'rejected'. This list will not be logged again: [<<"loopq">>,
    <0.1085.0>                                                                                                                                                                              <<"loopq">>]

如果环路上全是 expired 之类的非显式原因,RabbitMQ 会检测到并丢弃消息、日志里提示一次;含有 rejected 的环是允许的。

分步演示

死信队列
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / RabbitMQ 4.3.6 / Erlang/OTP 28.5 / pika 1.4.4(Homebrew 与临时虚拟环境);所有节点都是本机自己起的 RabbitMQ 进程(127.0.0.1,端口 9500 以上,每个实验独立的数据目录与节点名,不连任何外部服务);命令输出来自真实的 rabbitmqctl 与管理 API,发布与消费用 pika 的真实连接;计时用 time.perf_counter,数字每次运行会有小幅抖动。主队列都是经典队列;x-death 的内容用 pika 读出 headers;重试环里每次的间隔用 perf_counter 计
    • 没有验证:quorum 队列的 at-least-once 死信(dead-letter-strategy);delivery-limit 触发的死信(第 16 篇);集群里死信交换机在别的节点上;大量死信时的吞吐;死信消息的 expiration 属性是否被保留(只看了头和 body);x-death 数组很长时的头大小上限
    京ICP备2026067502号-1京公网安备11010502063487号