重试和失败传输必须分开配置,retry_strategy须显式嵌套在transport下而非根层级,failure_transport仅接收耗尽重试次数后仍失败的消息;漏配delay或作用域错误将导致重试失效或消息静默丢失。

重试和失败传输必须分开配,否则重试成功后消息可能直接进 DLQ,而不是回到原队列重试。
retry_strategy 必须显式写在 transport 下
Messenger 默认不启用重试,哪怕你写了 failure_transport 也没用。重试行为只由 transport 自身的 retry_strategy 控制,不是全局开关。
- 错误写法:把
retry_strategy放在framework.messenger根层级,它会被忽略 - 正确位置:必须嵌套在某个 transport(如
async)配置内部 - 关键参数必须成对出现:比如写了
max_retries,但漏了delay,Symfony 会报错The "delay" option is required when configuring a retry strategy - 常见组合示例:
transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy: max_retries: 3 delay: 1000 multiplier: 2 max_delay: 10000 jitter: 0.1
failure_transport 不等于重试终点
failure_transport 是最终失败后的归宿,不是重试过程中的中转站。它只接收那些已耗尽所有重试机会、仍抛出异常的消息。
- 它和
retry_strategy是解耦的:你可以有重试但没配failure_transport,此时最终失败消息会被丢弃(无日志、不可查) - 典型配置是接一个独立队列,比如
doctrine://default?queue_name=failed或amqp://.../failed - 不要把
failure_transport指向同一个 DSN:比如async和failed都用amqp://...,会导致死信消息又被消费者拉走重试,形成循环 - 验证是否生效:运行
php bin/console messenger:failed:show,有输出才说明失败消息真进了 DLQ
重试触发只看未捕获异常,不看返回值
Handler 方法里 return false、null 或 throw new \LogicException(),效果完全不同——只有后者才会触发重试。
- 业务逻辑中需要主动判断失败场景,例如数据库更新失败、第三方 API 返回 503,然后
throw new \RuntimeException('API unavailable') - 如果用了
try/catch却没 re-throw,重试机制完全不会启动 - 某些异常类型(如
TransportException)会被 Messenger 自动识别为瞬时错误,更倾向重试;而ValidationException这类通常不该重试,需在 handler 里提前拦截 - 注意事务边界:若 handler 在 Doctrine transaction 内执行,异常抛出会导致事务 rollback,下次重试是干净状态
指数退避参数抖动(jitter)不是可选装饰
jitter: 0.1 看似微小,但在高并发场景下能显著降低“惊群效应”——即大量失败消息在同一时刻重试,压垮下游服务。
- 没有
jitter时,三次重试分别在 1s、2s、4s 后发生;加上jitter: 0.1后,实际延迟会在 ±10% 范围内随机浮动 - 生产环境建议始终开启,哪怕只是
jitter: 0.05 -
max_delay是硬上限:即使 multiplier 算出来是 15s,也会被截断到 10s(假设配置了max_delay: 10000) - 调试时可临时设
delay: 100+jitter: 0,方便肉眼观察重试节奏
重试次数、延迟曲线、失败归档路径这三者必须对齐;配错任意一环,轻则重试失效,重则消息静默丢失。尤其是 retry_strategy 的作用域和 failure_transport 的隔离性,最容易在迁移或合并配置时被忽略。


















