后台执行超长时间任务解决方案。
解决的问题
- 耗时较长
- 各端无法调取相关任务进度进行反馈
- 自定义任务过后反馈结果
- 请教下,Laravel 如何让程序在后台执行超长时间的代码?
流程简述
- 使用异步队列执行相关任务
- 使用助手方法进行任务/进度创建
- 通过暴露接口反馈相关进度
助手类源码如下
<?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 自带的报错异常我实在无力吐槽,不方便排错.
队列排错参考:
最后
- 代码上面的业务完全根据我自身项目编写,直接照搬 可能会引起不兼容。
- 分享 更多的是一种解决思路,希望能帮到后面的小伙伴。
- 如果对代码 有什么优化思路 或者 建议 也可以探讨下。
withoutOverlapping() 定时任务有这个新特性了