Laravel中如何将队列任务进度以百分比形式持久化存储到数据库?
- 内容介绍
- 文章标签
- 相关推荐
本文共计884个文字,预计阅读时间需要4分钟。
在任务类中使用 Eloquent 更新记录时,直接在行内更新即可,但应避免使用模型事件和事务处理。Laravel 队列默认不开启数据库事务,但如果在手动开启(如在 job handle 方法中使用 DB::transaction)时更新进度,进度更新可能会被回滚。
-
dispatch()时传入一个唯一$jobId或用$this->job->uuid()生成标识,避免多个相同任务互相覆盖 - 别在
__construct()里查或改模型——构造阶段模型状态不可靠,且部分驱动(如 Redis)会序列化对象,Eloquent 模型容易出错 - 用
DB::table('job_progress')->upsert()替代先查后 save,避免并发写冲突;字段至少包含job_uuid、progress、updated_at
Laravel 10+ 的 InteractsWithQueue 怎么配合进度更新
这个 trait 提供了 $this->job 实例,里面能拿到 uuid() 和当前尝试次数,是天然的进度锚点。但注意:它只在 handle() 执行时才完整可用,不能在 failed() 或构造函数里依赖它。
-
$this->job->uuid()是最稳的 key,比自定义 ID 更可靠(尤其用 Horizon 时) - 如果任务分多步(比如处理 1000 条数据),每处理 100 条就调一次
DB::table(...)->where('job_uuid', $this->job->uuid())->update(...),别等全做完再写 - 别在
failed()方法里重置进度为 0——用户可能重试,该保留上次成功进度
为什么用缓存存进度反而容易丢数据
Redis 缓存快,但队列失败重试、机器重启、TTL 过期都会让进度消失。生产环境要持久化,就得落库,缓存只适合做临时高频读(比如前端轮询)。
- 不要用
Cache::put('progress_'.$uuid, $p, 3600)当主存储——没事务保证,也没历史可查 - 如果真要用缓存加速读,写库后立刻
Cache::forever('progress_'.$uuid, $p),但读的时候必须 fallback 到 DB 查询 - Redis 持久化开关(RDB/AOF)不是保险柜,队列进程崩溃时最后几次 update 可能根本没刷到磁盘
前端轮询 /api/progress/{uuid} 接口要注意什么
这个接口本质是查数据库,不是查内存或缓存,所以必须加索引,否则并发一高就拖慢整个队列消费。
- 给
job_progress.job_uuid加唯一索引或普通索引,避免全表扫描 - 返回结构保持极简:
{"progress": 65, "status": "processing"},别塞模型关系或额外字段 - 别在接口里触发重试逻辑或写操作——纯读,否则和队列写进度形成竞争条件
真正难的不是存百分比,而是保证「写进度」和「任务执行流」在各种失败场景下不脱节。比如任务卡住、超时 kill、Horizon 手动停止,这些时候进度值是否还可信,得靠 uuid + 时间戳 + 状态字段组合判断,单靠一个数字撑不住。
本文共计884个文字,预计阅读时间需要4分钟。
在任务类中使用 Eloquent 更新记录时,直接在行内更新即可,但应避免使用模型事件和事务处理。Laravel 队列默认不开启数据库事务,但如果在手动开启(如在 job handle 方法中使用 DB::transaction)时更新进度,进度更新可能会被回滚。
-
dispatch()时传入一个唯一$jobId或用$this->job->uuid()生成标识,避免多个相同任务互相覆盖 - 别在
__construct()里查或改模型——构造阶段模型状态不可靠,且部分驱动(如 Redis)会序列化对象,Eloquent 模型容易出错 - 用
DB::table('job_progress')->upsert()替代先查后 save,避免并发写冲突;字段至少包含job_uuid、progress、updated_at
Laravel 10+ 的 InteractsWithQueue 怎么配合进度更新
这个 trait 提供了 $this->job 实例,里面能拿到 uuid() 和当前尝试次数,是天然的进度锚点。但注意:它只在 handle() 执行时才完整可用,不能在 failed() 或构造函数里依赖它。
-
$this->job->uuid()是最稳的 key,比自定义 ID 更可靠(尤其用 Horizon 时) - 如果任务分多步(比如处理 1000 条数据),每处理 100 条就调一次
DB::table(...)->where('job_uuid', $this->job->uuid())->update(...),别等全做完再写 - 别在
failed()方法里重置进度为 0——用户可能重试,该保留上次成功进度
为什么用缓存存进度反而容易丢数据
Redis 缓存快,但队列失败重试、机器重启、TTL 过期都会让进度消失。生产环境要持久化,就得落库,缓存只适合做临时高频读(比如前端轮询)。
- 不要用
Cache::put('progress_'.$uuid, $p, 3600)当主存储——没事务保证,也没历史可查 - 如果真要用缓存加速读,写库后立刻
Cache::forever('progress_'.$uuid, $p),但读的时候必须 fallback 到 DB 查询 - Redis 持久化开关(RDB/AOF)不是保险柜,队列进程崩溃时最后几次 update 可能根本没刷到磁盘
前端轮询 /api/progress/{uuid} 接口要注意什么
这个接口本质是查数据库,不是查内存或缓存,所以必须加索引,否则并发一高就拖慢整个队列消费。
- 给
job_progress.job_uuid加唯一索引或普通索引,避免全表扫描 - 返回结构保持极简:
{"progress": 65, "status": "processing"},别塞模型关系或额外字段 - 别在接口里触发重试逻辑或写操作——纯读,否则和队列写进度形成竞争条件
真正难的不是存百分比,而是保证「写进度」和「任务执行流」在各种失败场景下不脱节。比如任务卡住、超时 kill、Horizon 手动停止,这些时候进度值是否还可信,得靠 uuid + 时间戳 + 状态字段组合判断,单靠一个数字撑不住。

