当Laravel项目的队列任务越来越多,你会发现一个现象:发短信、发邮件这类高优先级任务,经常被大批量的报表导出、数据同步任务堵在后面,一等就是几分钟甚至更久。问题不在于队列性能不够,而在于所有任务都挤在同一条队列里,消费者只有一个进程,先进先出谁也插不了队。解决思路是把任务按优先级拆分到不同队列,再用独立的worker进程分别消费。下面结合Docker的容器化部署,完整讲一遍怎么做。

一、先搞清楚Laravel队列优先级的实现原理
Laravel本身没有"优先级"这个配置项,它的优先级完全靠队列的划分和worker的启动顺序来体现。默认情况下,所有没有指定队列的任务都会进入default队列。你可以通过Job类或任务分发时指定队列名称,比如在Job类里定义public $queue = 'emails';,或者在分发时链式调用onQueue('emails'),任务就会被推入Redis或数据库中对应的队列键里。
消费端有两种典型做法。第一种是单进程监听多个队列:php artisan queue:work --queue=high,default,worker会优先从high队列取任务,high空了才去处理default。这种方式简单,但有个明显缺陷:当default队列任务执行时间很长时,新进来的high任务依然要等当前任务跑完。第二种就是给不同队列分配独立的worker进程,high队列独享一组worker,即使default堆积成山也不影响high的处理速度。生产环境推荐第二种,Docker的多容器特性正好适合这种模式,每个队列起一个容器实例,互不干扰,还能独立扩缩容。
顺便提一句Horizon。如果你的队列驱动是Redis,Horizon提供了非常好用的监控面板和进程配置管理,它通过配置文件声明supervisor进程组,本质上就是把上面的多队列worker编排收敛到了一份配置里。后面会分别给出原生方式和Horizon方式的部署写法,二选一即可。
二、队列拆分与任务分发的具体配置
先做好代码层的准备。假设我们把任务分为三个优先级:critical(支付回调、短信通知)、default(普通业务任务)、low(报表、数据同步)。以Redis为队列驱动,在.env中配置:
QUEUE_CONNECTION=redis REDIS_HOST=redis REDIS_PORT=6379
注意REDIS_HOST填的是Docker网络内的服务名,不是127.0.0.1,这是容器化部署时最常见的连接错误。接下来在Job类中指定队列:
<?php
namespace App\Jobs;
class SendSms implements ShouldQueue
{
// 指定该任务进入critical队列
public $queue = 'critical';
public function handle(): void
{
// 短信发送逻辑
}
}除了在Job类里静态声明,也可以在分发时动态指定,灵活性更高:
// 动态指定队列
SendSms::dispatch($phone, $content)->onQueue('critical');
// 延迟任务同样可以指定队列
GenerateReport::dispatch($userId)->delay(now()->addMinutes(5))->onQueue('low');建议在config/queue.php中把队列连接的retry_after设置得比任务最长执行时间略长,比如任务最长跑10分钟,就设成720秒,否则任务还没跑完就被判定超时重新入队,会出现重复执行的诡异问题。
三、docker-compose编排多队列容器
进入重点部分。核心思路是为每个队列定义独立的容器服务,各自执行queue:work命令。下面是一份可以直接落地的编排示例:
version: "3.8"
services:
app:
build: .
image: my-laravel-app
volumes:
- ./:/var/www/html
depends_on:
- redis
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
# 高优先级队列,2个并发进程
queue-critical:
image: my-laravel-app
command: php artisan queue:work redis --queue=critical --sleep=1 --tries=3 --max-time=3600
restart: unless-stopped
volumes:
- ./:/var/www/html
depends_on:
- redis
# 默认优先级队列,2个并发
queue-default:
image: my-laravel-app
command: php artisan queue:work redis --queue=default --sleep=2 --tries=3 --max-time=3600
restart: unless-stopped
volumes:
- ./:/var/www/html
depends_on:
- redis
# 低优先级队列,1个并发,sleep拉长降低资源占用
queue-low:
image: my-laravel-app
command: php artisan queue:work redis --queue=low --sleep=5 --tries=1 --max-time=3600
restart: unless-stopped
volumes:
- ./:/var/www/html
depends_on:
- redis
volumes:
redis-data:几个参数值得解释。--max-time=3600让worker进程每小时主动退出一次,配合restart: unless-stopped实现优雅重启,这是官方推荐的避免内存泄漏的手段,长驻进程不重启迟早会涨内存。--sleep控制队列空闲时的轮询间隔,低优先级队列可以设大一些省CPU。--tries=3限制重试次数,防止坏任务无限循环。如果单个容器内想跑多进程,可以把command换成用supervisord或者php artisan queue:work ... --workers=2(Laravel 11+支持多worker),不过容器化场景下更推荐用docker compose up -d --scale queue-default=2横向扩容,弹性更好。
如果选择Horizon方案,把队列容器换成一条命令即可,进程编排放配置文件里管理:
horizon:
image: my-laravel-app
command: php artisan horizon
restart: unless-stopped
volumes:
- ./:/var/www/html
depends_on:
- redis对应的config/horizon.php中定义supervisor:
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['critical'],
'maxProcesses' => 4,
'balance' => 'simple',
'tries' => 3,
],
'supervisor-2' => [
'connection' => 'redis',
'queue' => ['default', 'low'],
'maxProcesses' => 2,
'tries' => 3,
],四、上线后的几个排查要点
部署完成后,任务丢失是最先要排查的问题。确认Redis容器数据卷已挂载,否则容器重建后未消费的任务会全部蒸发。代码更新后记得重启队列容器,queue:work是常驻内存的,不会自动加载新代码,用php artisan queue:restart配合容器内的调度,或者直接docker compose restart queue-critical queue-default queue-low。
其次是失败任务的去向。给所有队列worker加上--failed-driver=database-uuid(默认配置),并确保failed_jobs表已迁移创建,配合queue:retry命令可以补救。日志方面,建议在容器内把日志写到stdout,Docker的日志驱动会统一收集,比在容器文件系统里写日志更利于排查。最后留意容器资源限制,给高优先级队列容器设置更高的CPU份额(compose里的cpu_shares),保证关键任务在宿主机繁忙时也能抢到资源。整体跑通后,你会得到一套优先级分明、可独立扩缩容的队列服务体系,后续再加新队列只需在compose里复制一段服务定义,改个队列名就行。