开多个worker无法保序,因各进程独立竞争消费任务,不保证dispatch先后顺序;真需顺序应改用单worker+任务链、数据库状态轮询或redis信号量协调。

并发任务本身和“顺序一致”是矛盾的——Laravel 的多 queue:work 进程天然打破顺序。真要顺序执行,就得主动放弃并发,或用状态协调替代队列顺序。
为什么开多个 worker 就没法保序
每个 queue:work 进程独立拉取任务,数据库或 Redis 驱动靠锁保证“不重复消费”,但不保证“按 dispatch 时间先后执行”。比如你连续 dispatch 5 个 ProcessOrderJob,它们可能被 3 个 worker 分别拿到、并行处理,完成时间完全取决于各自耗时和资源竞争。
- 即使所有任务进同一个队列(如
default),Laravel 也不提供 FIFO 严格保序——尤其在 database 驱动下,id自增 ≠ 消费顺序;Redis 驱动更依赖lpop原子性,但高并发下仍可能因网络延迟/重试打乱感知顺序 -
--sleep=1或--delay=0不能解决根本问题:这只是控制轮询节奏,不是调度策略 - 试图用
DB::table('jobs')->orderBy('id')->first()手动取任务?会绕过 Laravel 队列锁机制,引发重复执行或死锁
真正能保序的三种做法(按推荐度排序)
不是“怎么让并发保序”,而是“换一种方式表达顺序需求”:
-
单 worker + 任务链(
Bus::chain):最简单直接。启动唯一一个php artisan queue:work --queue=sequential,然后用Bus::chain([new Step1($id), new Step2($id)])->dispatch()->onQueue('sequential')。链内顺序由 Laravel 保证,失败中断,且所有步骤走同一队列、同一进程 -
数据库状态轮询(跨队列依赖):适合步骤分散在不同队列(如
emails,reports)。前置任务(SendEmailJob)成功后写status_tracking表标记completed;后置任务(GenerateReportJob)开头查该状态,未就绪则$this->release(60)延迟重试。注意设$tries = 3和合理$backoff,避免卡死 -
Redis 原子信号量(轻量高频场景):前置任务结尾执行
Redis::setex("dep:order_{$id}", 3600, 'done');后置任务用Redis::get("dep:order_{$id}")检查,为空则Redis::blpop("queue:waiting:order_{$id}", 30)等待。比 DB 轮询快,但需处理信号残留(比如用register_shutdown_function清理)
withChain 和 Bus::chain 到底选哪个
Bus::chain 是当前明确推荐的方式,withChain 已显陈旧:
-
Bus::chain支持->then()/->catch()回调,失败逻辑可集中处理;withChain的错误传播隐晦,容易漏掉中间任务失败 -
Bus::chain不依赖起始 Job 类,构造更自由:Bus::chain([new ProcessImage($id), new UploadToCDN($id)])->dispatch();而withChain必须从某个实例出发:(new ProcessImage($id))->withChain([...]),后续无法动态追加 - 两者都**不支持链中动态切队列**:想让第一步走
high、第二步走reports,必须在每个 Job 构造时硬编码onQueue('high')或onQueue('reports'),Bus::chain本身不提供 per-step 队列配置接口
顺序不是靠开更多进程实现的,而是靠放弃部分并发自由、换用状态协调或串行链路来换取确定性。最容易被忽略的是:任务链里传模型实例会序列化失败,务必只传 ID 并在 handle() 中重新查询——否则你会在日志里反复看到 Serialization of 'Closure' is not allowed。











