laravel+rabbitmq死信无限循环源于三要素缺失:未设retry_limit或reroute_failed、死信路由参数未在原队列声明、业务异常被吞导致重试不触发。

死信消息无限循环,本质是没设 max-attempts 或没配对的死信路由,RabbitMQ 会不断把失败消息塞回原队列。
为什么 Laravel + RabbitMQ 会出现死信无限循环?
常见于两种配置错位:
- 开了
retry.enabled: true,但max-attempts没设或设为0(Spring Boot 默认是 3,Laravel 的laravel-queue-rabbitmq默认不启用重试,必须显式开) - 启用了死信队列,但没关掉原队列的自动重入机制(比如
basic.nack(requeue=true)或未配置reroute_failed),导致消息被拒后又立刻重发 - 业务代码里
try/catch吞掉了异常,没让框架感知失败,重试逻辑压根不触发,而消息因超时或手动 reject 又被送回队列
Laravel 配置最大重试次数(max-attempts)的关键位置
注意:Laravel 原生队列驱动不支持重试次数限制,必须用 laravel-queue-rabbitmq 扩展,并在 config/rabbitmq.php 中配置:
-
'retry_limit' => 3:这是该扩展识别的重试上限字段(不是 Laravel 通用的tries属性) -
'retry_delay' => 1000:每次重试前等待毫秒数(可选) - 必须同时开启
'reroute_failed' => true,否则重试耗尽后消息会被丢弃,而不是进死信队列 - 若用 Horizon,它会覆盖部分配置,需检查
horizon.php中是否设置了tries或禁用了失败重路由
死信队列必须绑定正确的参数,否则消息不进死信只打转
光有死信交换机和队列不够,原业务队列得明确告诉 RabbitMQ:“失败后往哪儿送”。关键参数要写在队列声明时,不是交换机上:
- 原队列声明中必须带
x-dead-letter-exchange(如"failed-exchange") - 必须带
x-dead-letter-routing-key(如"application-x.order.failed",与failed_routing_key匹配) - 如果用了 TTL 实现延迟重试,还要加
x-message-ttl,但注意:TTL 过期触发的死信,和重试耗尽触发的死信,走的是同一套死信路由,别混用逻辑 - 验证方法:在 RabbitMQ 管理界面点开你的业务队列 → “Features” 标签页 → 看
Dead letter exchange和Dead letter routing key是否已填且拼写一致
最容易被忽略的坑:消费者没抛异常,重试形同虚设
很多同学以为只要配了 retry_limit 就万事大吉,结果发现消息还是无限循环。真实原因是:
- 业务逻辑里写了
try { ... } catch (\Exception $e) { Log::error($e); },异常被吞,框架认为“消费成功”,不会触发重试 - 正确做法是:不 catch,或 catch 后重新 throw,或调用
$this->fail()(Laravel 队列任务中) - 如果你用的是
Job类,确保它没实现ShouldBeUnique且没卡在去重锁里——这种失败不会走重试,而是直接进 failed_jobs 表 - 测试时用
php artisan queue:work --stop-when-empty观察单条消息生命周期,比看日志更直观
真正起作用的永远不是“开了重试”,而是“失败能被准确识别 + 重试有终点 + 死信有去处”这三者严丝合缝。少一个,消息就在队列里打转。











