Laravel 的队列系统允许把耗时任务从请求周期中剥离出来,但默认情况下所有任务都进入同一条队列。当订单通知、报表生成、消息推送等业务共享一个消费通道时,慢任务或高频任务很容易挤占资源。要解决这个问题,可以按业务模块划分独立队列,让每个模块拥有自己的任务通道和消费进程。本文从配置、任务绑定、消费进程和监控几个角度介绍具体做法。

为什么需要按业务模块拆分队列
队列的核心价值是削峰填谷和解耦,但单一队列并不适合所有场景。例如电商系统中,下单成功后需要发送邮件、生成发票、同步库存、推送 App 消息。如果这些任务都放在 default 队列,而生成发票的接口偶尔响应缓慢,后续的邮件和推送任务就会被阻塞,用户可能迟迟收不到通知。
独立队列能带来三个直接好处。第一,资源隔离,某个模块的任务量突增不会影响其他模块。第二,独立伸缩,可以为耗时较长的报表队列分配更多 worker 进程,而为实时性要求高的通知队列保留专门的快速消费进程。第三,故障定位更清晰,当队列堆积时可以快速知道是哪个业务模块出了问题,而不是面对一锅粥式的混合任务列表。
从实现角度看,Laravel 的队列拆分并不需要额外扩展包,核心只涉及两个概念:任务类上的 queue 属性,以及消费进程启动时通过 --queue 参数指定的队列名称。理解这两个概念后,就可以灵活组合出适合自己项目的队列拓扑。
为任务类绑定独立队列名称
Laravel 任务类实现 ShouldQueue 接口后,默认会进入连接配置中定义的默认队列。要改变这一行为,最直接的方式是在任务类中定义 public $queue 属性。下面是一个订单通知任务类的示例:
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendOrderNotification implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $queue = 'orders';
protected $order;
public function __construct($order)
{
$this->order = $order;
}
public function handle()
{
// 执行订单通知发送
}
}
这样无论在哪里分发 SendOrderNotification,它都会进入 orders 队列。如果希望不同任务类在同一个模块内共享队列,只需要设置相同的队列名称即可。
如果不想在任务类中写死队列名,也可以在分发时动态指定,这一方式更适合需要根据业务参数临时切换队列的场景。下面展示动态指定队列的写法:
use App\Jobs\SendOrderNotification;
use App\Jobs\ProcessReport;
SendOrderNotification::dispatch($order)->onQueue('orders');
ProcessReport::dispatch($report)->onQueue('reports');
需要注意的是,onQueue 方法只在任务类没有明确 $queue 属性时才会真正覆盖队列名称。如果任务类内部已经定义了 $queue,运行时再调用 onQueue 不会生效。因此建议团队约定统一规则:要么全部在任务类中集中声明队列,要么全部在分发端指定队列,避免两种方式混用造成认知混乱。
配置队列连接并让 Worker 独立消费
有了任务队列名称之后,还需要让消费进程只监听指定队列。Laravel 的 queue:work 命令支持 --queue 参数,多个队列可以用逗号分隔,并且左侧优先级更高。例如只处理订单队列:
php artisan queue:work redis --queue=orders
如果要同时处理订单和报表队列,但订单优先,可以这样写:
php artisan queue:work redis --queue=orders,reports --sleep=3 --tries=3 --timeout=60
这种方式的优点是一个进程可以消费多个队列,但如果某个队列长期有大量任务,它可能会持续占用进程,导致后面的队列得不到及时处理。因此更推荐在 Supervisor 中为每个业务模块创建独立的 program 配置,每个 program 只监听自己的队列,并设置不同的进程数量。
[program:laravel-orders-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan queue:work redis --queue=orders --sleep=3 --tries=3 --timeout=60 autostart=true autorestart=true user=www-data numprocs=2 redirect_stderr=true stdout_logfile=/var/log/laravel-orders-worker.log stopwaitsecs=3600 [program:laravel-reports-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --timeout=180 autostart=true autorestart=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/log/laravel-reports-worker.log stopwaitsecs=3600
上面的配置为订单队列启动 2 个进程,为报表队列启动 4 个进程,并且报表任务的超时时间设置为 180 秒。这样即使报表导出需要较长时间,也不会影响订单通知的及时消费。Supervisor 的 numprocs 可以根据业务压力独立调整,实现精细化的资源分配。
如果项目使用 Redis 作为队列驱动,还可以考虑使用 Laravel Horizon。Horizon 通过配置文件即可声明多个队列的消费策略,无需手动维护多个 Supervisor program。下面是一个 Horizon 环境配置示例:
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['orders', 'reports', 'messages'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
'timeout' => 60,
],
],
],
Horizon 的 balance 策略可以在多个队列之间自动平衡进程数量,避免某个队列独占过多 worker。不过 Horizon 只支持 Redis 驱动,使用数据库或其他驱动的项目仍需要依赖 Supervisor 或多进程管理工具。
独立队列的运维与避坑建议
队列拆分之后,运维侧需要关注每个队列的堆积情况。Laravel 本身不提供跨模块的监控面板,但可以通过定时任务统计 jobs 表或 Redis 队列长度,或者使用第三方工具如 Laravel Horizon 的仪表盘。一旦某个队列出现堆积,应优先排查该模块的任务执行时间、重试次数和异常日志,而不是直接重启所有 worker。
另一个容易忽略的问题是任务超时与 retry_after 的配合。不同业务模块的任务耗时差异较大,例如通知任务通常几秒内完成,而报表导出可能超过一分钟。如果使用相同的 retry_after 配置,长任务可能还在执行就被另一个 worker 重新拉取,导致重复执行。建议在任务类中通过 public $timeout 和 public $tries 属性单独控制,同时在队列连接配置中为不同连接设置合理的 retry_after。
失败处理也应按模块区分策略。例如支付结果通知失败后需要立即重试并报警,而统计报表失败可以只记录日志并在下个周期重新生成。可以利用 Laravel 的 failed 方法在任务类中实现自定义逻辑,也可以统一监听 Queue::failing 事件,根据任务所属队列或类名执行不同的报警策略。
总结:选择适合项目规模的拆分方式
如果团队人数较少、业务模块不超过三个,可以先用 onQueue 配合单个 worker 进程监听多个队列,通过逗号顺序控制优先级。这种方案实现成本最低,但需要接受高峰期可能出现的队列互相影响。
当系统规模扩大、任务量明显增长时,建议按照业务域拆分成多个独立队列,并为每个队列配置独立的 Supervisor program 或使用 Horizon 管理。这样做虽然增加了一些配置和维护成本,但能够显著降低故障影响范围,也让队列吞吐量和进程数量可以根据业务特性独立优化。