后台执行超长时间任务解决方案。

解决的问题

流程简述

  • 使用异步队列执行相关任务
  • 使用助手方法进行任务/进度创建
  • 通过暴露接口反馈相关进度

助手类源码如下

<?php
// +----------------------------------------------------------------------
// | Do what we can do
// +----------------------------------------------------------------------
// | Date  : 2019/9/11 - 9:25 AM
// +----------------------------------------------------------------------
// | Author: seebyyu <seebyyu@gmail.com> :)
// +----------------------------------------------------------------------

namespace App\Lib\Support;

trait MissionFrom
{

    /**
     * 标记前缀 模块名称#业务模块#板块标记
     *
     * @var string
     */
    public $prefix = 'school:task:default';

    /**
     * 任务详情
     * @var array
     */
    public $original = [];

    /**
     * Redis 链接
     *
     * The Redis factory implementation.
     *
     * @var \Illuminate\Redis\Connections\Connection
     */
    protected $redis;

    /**
     * 任务存在有效期
     *
     * @var int
     */
    protected $seconds = 600;

    /**
     * 创建任务
     *
     * @param string $sheet
     * @param int $len 总长度
     * @return string
     */
    public function createTask($sheet = '', $len = 100)
    {

        $sheet = $sheet ?: $this->sheet();

        $detail = [
            //  开始时间
            'begin' => time(),
            //  标记号
            'sheet' => $sheet,
            //  总长度
            'total_len' => $len,
            //  当前长度
            'schedule' => 0
        ];

        //  主体信息
        $this->connect()->setex($this->prefix. ':'. $sheet, $this->seconds, serialize($detail));

        //  初始化任务进度
        $this->connect()->setex($this->prefix. ':schedule:'. $sheet, $this->seconds, 1);

        return $sheet;
    }

    /**
     * 设置任务内容
     *
     * @param $sheet
     * @param $value
     * @return MissionFrom
     */
    public function setTaskContent($sheet, $value)
    {

        if( $this->connect()->exists($this->prefix. ':'. $sheet)){
            $this->connect()->setex($this->prefix. ':content:'. $sheet, $this->seconds, serialize($value));
        }

        return $this;
    }

    /**
     * 获取任务内容
     *
     * @param $sheet
     * @return MissionFrom
     */
    public function getTaskContent($sheet)
    {
        return empty($data = $this->connect()->get($this->prefix. ':content:'. $sheet)) ? null : unserialize($data);
    }

    /**
     * 设置任务前缀
     *
     * @param string $prefix
     * @return $this
     */
    public function setPrefix($prefix = '')
    {
        $this->prefix = 'school:task:'. ($prefix ?: 'default');

        return $this;
    }

    /**
     * 任务详情
     *
     * @param string $sheet
     * @return array
     */
    public function taskDetail($sheet = '')
    {

        $detail = $this->connect()->get($key = ($this->prefix. ':'. $sheet));

        if( !empty($detail)){

            $this->original = array_merge( unserialize($detail), [
                'schedule' => (int)$this->getSchedule($sheet),
                'content' => $this->getTaskContent($sheet)
            ]);
        }

        return (array) $this->original;
    }

    /**
     * 进度递增
     *
     * @param string $sheet
     * @return int
     */
    public function increments($sheet = '')
    {
        $inc = 0;

        if( !empty($detail = $this->taskDetail($sheet)) &&
            $detail['schedule'] < $detail['total_len']){

            $inc = $this->connect()->incr($this->prefix. ':schedule:'. $sheet);
        }

        return $detail['schedule'] ?? $inc;
    }

    /**
     * 获取任务进度
     *
     * @param string $sheet
     * @return string
     */
    public function getSchedule($sheet = '')
    {
        return $this->connect()->exists($key = ($this->prefix. ':schedule:'. $sheet)) ? $this->connect()->get($key) : 0;
    }

    /**
     * 生成任务单号
     */
    private static function sheet()
    {
        return md5(\Hash::make(date('YmdHis')));
    }

    /**
     * 所有任务进度
     *
     * @return array
     */
    public function taskAll()
    {

        $task_group_list = [];

        //  分组
        foreach( (array)$this->connect()->keys('school:task:*') as $task) {

            if( count($task_item = explode(':', $task)) == 4){

                list($model, $model_name, $business, $key) = $task_item;

                $task_group_list[$business][] = $this->setPrefix($business)->taskDetail($key);
            }

        }

        return $task_group_list;
    }

    /**
     * @return \Illuminate\Foundation\Application|mixed
     */
    public function connect()
    {
        return app('redis.connection');
    }
}

调用过程如下

<?php

namespace App\Jobs;

use App\Lib\Support\MissionFrom;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

/**
 * Excel 导入
 *
 * Class importExcel
 * @package App\Jobs
 */
class importExcel implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MissionFrom;

    /**
     * 任务运行的超时时间。
     *
     * @var int
     */
    public $timeout = 300;

    /**
     * @var string
     */
    public $sheet;

    /**
     * importExcel constructor.
     * @param $sheet
     */
    public function __construct($sheet = '')
    {
        $this->sheet = $sheet;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {

        //  自定义业务前缀
        $prefix = 'export_students';

        //  创建任务进度
        $this->sheet = $this->setPrefix($prefix)->createTask($this->sheet, 20);

        //  开始执行任务
        echo '任务开始:'. $this->sheet. "\n";

        for ($i = 1; $i <= 20; $i++){

            //  延时模拟长时间任务
            sleep(rand(1, 2));

            //  进度 +1
            echo '任务进度:'. ($this->setPrefix($prefix)->increments($this->sheet)). "\n";
        }

        //  追加结果 任何类型
        $this->setPrefix($prefix)->setTaskContent($this->sheet, [
            'url' => 'http://www.baidu.com'
        ]);

    }

}

控制器部分

....
    /**
     * 学校pc端后台任务进度列表
     *
     * @return array
     */
    public function duties()
    {

        if( empty($key = request('key'))){

            $key = md5(\Hash::make(date('YmdHis')));

            //  创建任务
            $this->dispatch(new importExcel($key));

            return $key;
        }else{
            //  查询单条任务信息
            //  $this->setPrefix('export_students')->taskDetail($key);

            return success(['data' => array_merge([
                //  导出每餐记录列表
                'meal_records' => [],
                //  每日记录列表
                'daily_records' => [],
                //  其他记录列表
                'other_records' => [],
                //  照片库
                'photo_gallery' => [],
                //  采购计划
                'purchasing_plan' => [],
                //  凭证记录
                'voucher_records' => [],
                //  食材库
                'ingredient_records' => [],
                //  导入学生
                'import_students' => [],
                //  导出学生
                'export_students' => []
            ], $this->taskAll())]);
        }
    }
    ....

达到的效果

耗时任务解决思路分享

注意事项

  • QUEUE_DRIVER=sync 变更为 redis
  • 开发阶段强烈建议把 horizon 这玩意儿装上,Laravel 自带的报错异常我实在无力吐槽,不方便排错.

队列排错参考:

Laravel 队列:如何查看队列报错信息?

最后

  • 代码上面的业务完全根据我自身项目编写,直接照搬 可能会引起不兼容。
  • 分享 更多的是一种解决思路,希望能帮到后面的小伙伴。
  • 如果对代码 有什么优化思路 或者 建议 也可以探讨下。
i@llons.com
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

withoutOverlapping() 定时任务有这个新特性了

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!