Laravel通过队列异步处理长耗时任务,禁止在控制器中同步执行;需封装为可中断、可重试、可监控的Job类,配合Supervisor守护worker进程,并合理配置max-jobs、max-time、重试机制与队列分发策略。

Laravel 本身不直接“处理”长耗时任务,而是通过 queue 将其异步移交到后台工作进程;Supervisor 不是 Laravel 内置组件,它只是 Linux 上稳定守护 php artisan queue:work 进程的通用工具——用错位置或配置不当,队列照样卡死、任务丢失、内存爆满。
为什么不能直接在控制器里跑 sleep(300) 或大循环?
PHP-FPM 进程会阻塞,Web 请求超时(Nginx 默认 60s,Apache 类似),用户看到 504;同时数据库连接可能被回收、日志写入中断、异常无法被捕获上报。真正要跑的不是“代码”,是“可中断、可重试、可监控”的队列任务。
必须把耗时逻辑封装进 App\Jobs\YourLongRunningJob,然后用 dispatch() 推进队列:
use App\Jobs\ProcessMonthlyReport;
ProcessMonthlyReport::dispatch($userId)->onQueue('reports');
关键点:
-
onQueue('reports')显式指定队列名,避免所有任务挤在default队列导致优先级混乱 - Job 类里不要用
$this->user = auth()->user()这类运行时依赖——序列化时会失败;改用传 ID,handle()中再查 - 避免在
__construct()中做 DB 查询或 HTTP 请求,只存原始参数
queue:work 进程为什么总自己退出?
常见原因不是代码报错,而是 Supervisor 配置没关掉自动重启策略,或 Laravel 的 QUEUE_WORKER_MAX_JOBS / QUEUE_WORKER_MAX_TIME 触发了优雅退出。默认 php artisan queue:work 每处理 250 个任务或运行 60 分钟就会退出——这是防止内存泄漏的保护机制,不是 bug。
Supervisor 需显式配置为“自动拉起退出的进程”,且禁用 autostart=false 以外的干扰项:
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/your-app/artisan queue:work --queue=reports,default --sleep=3 --tries=3 --max-jobs=250 --max-time=3600 autostart=true autorestart=true user=www-data numprocs=2 redirect_stderr=true stdout_logfile=/var/log/supervisor/worker.log
注意:
-
--max-jobs=250和--max-time=3600要和 Laravel 的config/queue.php中options里的值对齐,否则 supervisor 看不到进程退出原因 -
numprocs=2表示起两个独立 worker,但它们共享同一个--queue=reports,default,Laravel 内部会轮询分发,不是手动负载均衡 - 别漏掉
--sleep=3:空闲时每 3 秒轮询一次 Redis/DB,太小加重 IO,太大响应延迟
Redis 队列爆满、任务堆积、retry 堆成山怎么办?
先看 redis-cli -a yourpass llen queues:default,如果数字持续 > 1000,说明消费跟不上生产。不是加 worker 数量就能解决——更可能是单个任务执行太久(比如一个 PDF 生成要 80 秒),堵住整个队列。
排查路径:
- 用
php artisan queue:failed查失败任务,重点看exception字段是否含TimeoutException或PDOException: Lost connection - 检查任务中是否有同步 HTTP 调用(
file_get_contents、curl_exec)没设超时,应强制加timeout=10 - DB 查询是否缺索引?
DB::listen()记录慢查询,>500ms 的必须优化 - 避免在 job 中调
sleep()等待外部结果——改用「状态轮询 + delayed dispatch」模式
例如第三方 API 返回 processing 状态,就不要 while 循环等,而应 dispatch 自己带 delay:
if ($status === 'processing') {
self::dispatch($jobId)->delay(now()->addSeconds(30));
}
Supervisor 只管进程不死,不管任务逻辑是否合理;队列积压本质是业务节奏和 worker 吞吐不匹配,或是任务设计违反了异步原则。最常被忽略的一点:没有给每个关键 job 加 public $tries = 3; 和 public $backoff = 60;,导致失败后立即重试,雪崩式打垮下游服务。










