dispatch()后拿不到实时进度,因Laravel队列不自动记录中间状态,需任务内用Redis原子操作(如INCRBY)主动上报已完成数,并由前端按总量换算百分比。

为什么 dispatch() 后拿不到实时进度
因为 Laravel 队列本身不记录任务执行中的中间状态,dispatch() 只是把任务推入队列,后续由 worker 拉取并执行——整个过程默认无“进度”概念。所谓“百分比”,必须靠你自己在任务内部主动上报,再配合外部存储(如 Redis、DB)做状态快照。
常见错误现象:Job::dispatch()->onQueue('default') 后立刻查数据库或缓存,发现没数据;或者用 job->progress 这种不存在的属性直接报错。
- 进度不是 Laravel 内置字段,
$this->progress会报Undefined property - 不要依赖队列驱动(如 database、redis)自动记录执行阶段——它们只管投递和完成,不管“做到哪了”
- 如果任务分多步但每步耗时不均(比如前 90% 是 IO 等待,后 10% 是 CPU 密集),按步骤数硬算百分比会严重失真
用 Redis + incrby 实现轻量级进度更新
Redis 的原子操作适合高频写入且无需事务保障的场景,比 DB 写入快一个数量级,也避免并发覆盖问题。关键不是存“百分比”,而是存“已完成单位数”,再由前端/接口按总量换算。
假设你有个导出 Excel 的任务,总行数已知为 $totalRows:
public function handle()
{
$jobId = $this->job->getJobId(); // 或用 uniqid() + 任务参数哈希
$progressKey = "job:progress:{$jobId}";
<pre class="brush:php;toolbar:false;">Redis::set($progressKey . ':total', $this->totalRows);
foreach ($this->rows as $index => $row) {
// 处理单行...
$done = $index + 1;
Redis::incrby($progressKey . ':done', 1); // 原子自增
// 每 10 行同步一次,避免 Redis 请求爆炸
if ($done % 10 === 0 || $done === $this->totalRows) {
$percent = round(($done / $this->totalRows) * 100);
Redis::setex($progressKey . ':percent', 3600, $percent);
}
}
}
- 别用
SET覆盖:done,要用INCRBY,否则并发时会丢进度 - 不要在每次循环都写
:percent,Redis 频繁写入会拖慢任务,用定时采样更稳 -
jobId必须全局唯一,不能只用getJobId()(它在重试时会变),建议拼接任务类名+参数 hash
如何让前端安全读取进度而不轮询爆 Redis
前端直接 GET /api/job/progress?job_id=xxx 没问题,但得控制频率和缓存策略。重点不是“怎么查”,而是“查不到时怎么兜底”。
后端接口示例(Laravel Controller):
public function progress(Request $request)
{
$jobId = $request->input('job_id');
$key = "job:progress:{$jobId}";
<pre class="brush:php;toolbar:false;">$done = (int) Redis::get($key . ':done') ?: 0;
$total = (int) Redis::get($key . ':total') ?: 0;
// 如果 total 为 0,说明任务还没初始化好,返回 0 或 null 更合理
if ($total === 0) {
return response()->json(['percent' => 0, 'status' => 'pending']);
}
$percent = $done >= $total ? 100 : round(($done / $total) * 100);
return response()->json([
'percent' => $percent,
'done' => $done,
'total' => $total,
'status' => $done >= $total ? 'completed' : 'running'
]);
}
- 前端轮询间隔至少 1.5 秒起,短于 1 秒容易触发 Redis QPS 限流(尤其共享环境)
- 如果
:total查不到,别猜、别默认设 100,直接返回pending,否则前端会显示“100% 但实际卡住” - 别在响应里塞 Redis key 名,更不要暴露
job:progress:xxx这种结构,防止被恶意探测
重试、失败、超时场景下进度怎么不“回滚”也不“卡死”
队列任务重试时,handle() 会重新执行,但你不能让进度从 0 开始——否则用户看到“30% → 0% → 10%”就懵了。核心原则:进度只增不减,且失败后要标记状态,而非清空。
- 在
failed()方法里,只设status: failed,不要删:done或:percent,让用户知道“卡在哪儿了” - 重试时先检查
:done是否已存在,若存在则跳过已处理部分(需你在逻辑里支持断点续传,比如加 whereid > $lastDoneId) - 给进度 key 加过期时间(
SETEX),但别设太短(比如 60 秒),否则任务跑 20 分钟,中途 Redis 清掉 key 就彻底丢失进度 - 超时(
timeout配置)后 worker 强制 kill 进程,此时 Redis 写入可能中断,所以务必用INCRBY+ 定期采样,而不是依赖最后一步写入
最麻烦的其实是“总量动态变化”——比如边查库边导出,总数一开始根本不知道。这种就得改方案:先扫一遍计数,或用流式估算(如每万条上报一次,再按比例外推),不能硬套固定百分比模型。











