一个请求里做三件事:存数据、发邮件、调第三方接口。前两件很快,第三件可能超时。把耗时操作放进队列,接口就能立刻返回,失败的任务还能重试。这篇文章讲实际项目里最容易出问题的几个点。

Laravel 队列与任务调度:让耗时任务可靠地异步执行 示意图

什么时候该用队列

  • 耗时超过 200ms 且不要求同步结果的操作;
  • 可能失败的第三方调用,需要重试与记录;
  • 批量处理,例如导入 5 万行数据、生成多张图片;
  • 有明显波峰的任务,例如整点推送。

反过来说,用户点击后必须立刻看到结果的操作不适合丢队列,否则只能靠轮询或 WebSocket 补偿。

任务类怎么写

class SendOrderMail implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public int $orderId) {}

    public function handle(Mailer $mailer): void
    {
        $order = Order::find($this->orderId);

        if (! $order) {
            return; // 数据已删除,直接结束,避免无意义重试
        }

        $mailer->send(new OrderShipped($order));
    }

    public function failed(Throwable $e): void
    {
        logger()->error('发送订单邮件失败', [
            'order' => $this->orderId,
            'message' => $e->getMessage(),
        ]);
    }
}

几个细节值得注意:

  • 构造函数里只放标量或可序列化的 ID,不要直接传模型实例,避免序列化后数据过期;
  • tries 与 backoff 控制重试节奏,第三方接口不稳定时给指数退避更合适;
  • failed() 只做记录与告警,不要在里面再次抛异常。

失败任务与监控

先执行 php artisan queue:failed-table 与迁移,失败任务才会落库。常用命令:

php artisan queue:work --tries=3 --timeout=60
php artisan queue:failed
php artisan queue:retry all

真正需要关注的是「失败任务持续增长」而不是单条失败。给失败表加一个定时检查,超过阈值就发告警,比事后翻日志有效得多。

调度任务的三个开关

Schedule::command('reports:daily')
    ->dailyAt('02:30')
    ->withoutOverlapping()
    ->onOneServer()
    ->runInBackground();
  • withoutOverlapping():上一次没跑完时跳过本次,防止任务堆积;
  • onOneServer():多台机器部署时只让一台执行,需要共享缓存锁;
  • runInBackground():不阻塞其他调度任务。

调度依赖系统的 crontab 每分钟执行 php artisan schedule:run,忘记加这一行是「定时任务不生效」最常见的原因。

驱动选择与部署

小项目用 database 驱动最省事,任务表就是数据库表,出问题可直接查询。任务量大时换 Redis,但要注意持久化配置,进程重启可能丢任务(视 retry_after 与持久化策略而定)。

生产环境用 Supervisor 守护 queue:work,并在部署脚本里执行 php artisan queue:restart,否则改了任务代码旧进程仍跑在旧代码上。这是上线后「改了没生效」的经典原因。

小结

队列不是「丢进去就完事」,它把同步的错误变成了异步的、需要监控的错误。任务幂等、失败告警、重启策略这三件事做好,队列才算真正可靠。排查接口超时时,可以配合请求生命周期里的耗时中间件,先确认慢在同步阶段还是任务阶段。