laravel队列重试间隔由$tries和$backoff共同决定,$backoff为数组时才实现指数退避,整数则为固定延迟;retryafter()仅影响单任务最小等待时间,failed()中sleep无效;redis驱动支持精准退避,database驱动存在时延偏差。

队列任务重试间隔由 $tries 和 $backoff 共同决定
Laravel 不会自动“指数退避”,默认是固定等待时间。所谓“指数退避”必须手动配置 $backoff 为数组,否则哪怕设了 $tries = 5,每次失败后都等同样秒数(比如全等 3 秒),根本不是指数增长。
常见错误现象:php artisan queue:work 日志里看到任务反复在相同时间点重试(如每 3 秒一次),但预期是 1s → 2s → 4s → 8s。
-
$tries = 5只控制最多执行几次(含首次),不控制间隔 -
$backoff = 3(整数)→ 每次失败后固定等待 3 秒 -
$backoff = [1, 2, 4, 8](数组)→ 第1次失败等1秒,第2次等2秒,第3次等4秒,第4次等8秒;数组长度必须 ≥$tries - 1,否则后续重试用最后一位值兜底 - 如果
$tries = 5但$backoff = [1, 2],那么第3、4次重试都会等 2 秒
retryAfter() 和 failed() 不能替代 $backoff
retryAfter() 是针对单个任务实例的「下次重试前最小等待时间」,它只在任务抛出 Throwable 且未被 failed() 捕获时生效;而 $backoff 是框架级调度策略,优先级更高、更稳定。
使用场景:你想让某个任务失败后至少等 10 秒再试(比如调第三方 API 频率受限),但又不想全局改 $backoff 数组 —— 这时才用 retryAfter()。
-
retryAfter()返回值必须是整数秒,返回null或不实现该方法 → 回退到$backoff或默认值 -
failed()里调sleep()或usleep()完全无效:进程已退出,队列 worker 已开始下一轮循环 -
failed()适合记录日志、发告警、清理资源,不适合控制重试节奏
Redis 队列和数据库队列对 $backoff 的处理差异
Redis 驱动(redis / redis-cluster)能精确按 $backoff 数组值延迟重试;而 database 驱动依赖 available_at 字段写入时间,受数据库时区、worker 启动频率影响,实际延迟可能偏差 ±1 秒甚至更多。
性能影响:database 队列在高并发重试场景下容易堆积 jobs 表,因为每次重试都要 update + select;Redis 则用 zset 排序,O(log N) 查找,更稳。
- 若用 database 驱动且要求严格退避节奏(比如金融类重试),建议换 Redis 或加一层自定义调度(如用
dispatchAt()手动控制) - Redis 中
$backoff = [1, 2, 4]会真实写入 zset 的 score 为当前时间戳 + 对应秒数,worker pop 时按 score 排序 - database 驱动下,
$backoff数组仍有效,但最终available_at值由 PHPtime() + $delay计算,若 worker 进程卡顿,可能造成批量任务“扎堆”重试
测试指数退避是否生效的最快方式
别等生产环境出问题才验证。本地用 sync 驱动测逻辑不行(它不走重试流程),必须用 redis 或 database,并配合 php artisan queue:work --once 单次执行 + 日志观察。
- 在任务的
handle()里写throw new RuntimeException('simulated fail'); - 启动 worker:
php artisan queue:work redis --verbose --delay=0(--delay=0确保不额外加延迟) - 看日志里
[2024-05-20 10:11:22] local.INFO: Processing job...和下一条的时间差是否匹配$backoff数组 - 注意:Laravel 9.22+ 修复了
$backoff在 Horizon 下被忽略的问题,旧版本需升级或手动 patchIlluminate\Queue\Jobs\RedisJob::release()
真正难调的不是配数组,而是不同驱动下时钟同步、worker 进程复用、以及失败后是否被其他中间件提前拦截 —— 这些地方一漏,$backoff 就形同虚设。











