重试策略必须显式配置在transport层,否则messenger:consume不会重试失败消息;需在transport中定义retry_strategy(如max_retries、delay等),且仅由未捕获异常触发,与返回值无关。

重试策略必须显式配置在 transport 层
默认情况下,messenger:consume 不会重试任何失败消息——哪怕你抛了 TransportException 或数据库异常。重试不是全局开关,而是每个 transport 的独立配置项。不写 retry_strategy,就等于禁用重试。
常见错误是只在 routing 里指定消息去哪个 transport,却漏掉 transport 自身的重试定义。比如:
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
# ❌ 缺少 retry_strategy 块 → 永远不重试
正确写法必须包含完整策略块:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 10000
jitter: 0.1
-
max_retries: 3表示最多再执行 3 次(含首次共 4 次) -
delay: 1000是第一次重试前等待 1000ms,第二次是 2000ms,第三次是 4000ms(因multiplier: 2) -
max_delay: 10000会截断后续增长,避免延迟无限拉长 -
jitter: 0.1让每次延迟在 ±10% 范围内随机浮动,防“惊群”
重试只由未捕获异常触发,和返回值无关
很多人误以为 handler 返回 false、空数组或 HTTP 错误码就能触发重试。实际上,Messenger 只看是否抛出未捕获异常。即使你手动 return 一个错误状态,只要没 throw,消息就被视为“成功处理”,直接确认并从队列移除。
典型踩坑场景:
- 调用外部 API 失败但只记录日志 +
return,不throw→ 消息丢失,无重试 - 数据库事务回滚后没 re-throw 异常 → 消息被消费,但业务未生效
- 用
try/catch吞掉所有异常却不重新抛出 → 重试机制完全失效
安全做法是:只要逻辑未达成预期结果,就明确 throw new RuntimeException('...')。Symfony 不关心异常类型,只判断是否未被捕获。
延迟队列 ≠ 重试延迟,需用不同 transport 实现
retry_strategy.delay 控制的是重试间隔,不是“把消息延后到某个时间点投递”。如果你需要定时任务(例如“订单创建 5 分钟后发提醒”),不能靠重试策略实现,得用专门的延迟 transport。
常见方案是 Redis + symfony/amqp-pack 或 symfony/redis-messenger,配合 delayed transport 配置:
transports:
delayed:
dsn: 'redis://localhost:6379/delayed'
options:
retry_strategy:
max_retries: 0 # 延迟队列本身不重试,由主 transport 控制
然后在发送时指定延迟:
$bus->dispatch( new OrderReminder($orderId) )->delay(5 * 60 * 1000); // 单位毫秒
注意:delay() 方法只对支持延迟的 transport 生效(如 Redis),Doctrine transport 默认不支持,强行调用会忽略延迟。
死信队列(DLQ)是重试耗尽后的兜底出口
当消息经历全部重试仍失败,它不会消失,也不会卡在原队列里。Messenger 会把它转发到 failure_transport —— 这就是死信队列。这个动作是自动的,但必须提前配好目标 transport。
配置示例:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
failure_transport: failed # ← 关键:指向另一个 transport
failed:
dsn: 'doctrine://default?queue_name=failed'
要点:
-
failure_transport必须是已声明的 transport 名称,不能是任意字符串 - 推荐用 Doctrine transport 存 DLQ,方便查表、重放或人工清理
- 消息进入 DLQ 后,
messenger:consume默认不会自动处理它;需单独运行php bin/console messenger:consume failed手动干预
真正容易被忽略的是:DLQ 不是“失败日志”,它是可操作的队列。你不配置 failure_transport,消息就在最后一次重试失败后被永久丢弃——连告警机会都没有。











