文档章节

使用YII2构建一个定时任务管理后台

botkenni
 botkenni
发布于 2018/05/03 12:42
字数 1725
阅读 234
收藏 2

首先介绍一下我遇到过的,个人觉得奇葩的极其不方便的定时任务方式

每当有一个定时任务需求就在linuxcrontab中注册一个任务

*/5 * * * * wget --spider "http://xxxxx.com/index.php?m=Kf&c=Task&a=recommendTasks"
*/2 * * * * wget --spider "http://xxxxx.com/index.php?m=Kf&c=Task&a=batchOneBuyCodesa"
*/5 * * * * wget --spider "http://xxxxx.com/index.php?m=Kf&c=Task&a=bathCardtradesd"
*/1 * * * * wget --spider "http://xxxxx.com/index.php?m=Kf&c=Task&a=pushg"

不知道有不有大兄弟躺枪了,希望你看了我的实现方式后,以后不要这么搞定时任务了,当然我的也不会是最好了,别钻牛角尖

这种方式的定时任务有什么问题?

  1. 显而易见的就是不知道这种鬼链接是什么个东西,想停不敢停怕背锅,久而久之就扔上面
  2. http请求的方式触发任务,任务多的时候占用webserver的资源(如果是以cli模式触发就算了,当我没说)
  3. 无法记录任务运行的状态,例如: 是否运行成功,运行一次耗时多少(你千万别跟我说在每个任务记录个里日志啥的好吧)

我将围绕如何解决以上三个问题来展开我的实现过程

  • 创建一个专门管理定时任务的表
CREATE TABLE `tb_crontab` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '定时任务名称',
  `route` varchar(50) NOT NULL COMMENT '任务路由',
  `crontab_str` varchar(50) NOT NULL COMMENT 'crontab格式',
  `switch` tinyint(1) NOT NULL DEFAULT '0' COMMENT '任务开关 0关闭 1开启',
  `status` tinyint(1) DEFAULT '0' COMMENT '任务运行状态 0正常 1任务报错',
  `last_rundate` datetime DEFAULT NULL COMMENT '任务上次运行时间',
  `next_rundate` datetime DEFAULT NULL COMMENT '任务下次运行时间',
  `execmemory` decimal(9,2) NOT NULL DEFAULT '0.00' COMMENT '任务执行消耗内存(单位/byte)',
  `exectime` decimal(9,2) NOT NULL DEFAULT '0.00' COMMENT '任务执行消耗时间',
  PRIMARY KEY (`id`)
) 
  • 所有任务通过一个入口方法来调度
* * * * * cd /server/webroot/yii-project/ && php yii crontab/index
  • 实现任务调度控制器 commands/CrontabController.php
<?php
namespace app\commands;

use Yii;
use yii\console\Controller;
use yii\console\ExitCode;
use app\common\models\Crontab;

/**
 * 定时任务调度控制器
 * @author jlb
 */
class CrontabController extends Controller
{

    /**
     * 定时任务入口
     * @return int Exit code
     */
    public function actionIndex()
    {
    	$crontab = Crontab::findAll(['switch' => 1]);
    	$tasks = [];

    	foreach ($crontab as $task) {

    		// 第一次运行,先计算下次运行时间
    		if (!$task->next_rundate) {
    			$task->next_rundate = $task->getNextRunDate();
    			$task->save(false);
    			continue;
    		}

    		// 判断运行时间到了没
    		if ($task->next_rundate <= date('Y-m-d H:i:s')) {
                $tasks[] = $task;
    		}
    	}

        $this->executeTask($tasks);

        return ExitCode::OK;
    }
    
    /**
     * @param  array $tasks 任务列表
     * @author jlb
     */
    public function executeTask(array $tasks)
    {

        $pool = [];
        $startExectime = $this->getCurrentTime();

        foreach ($tasks as $task) {
            
            $pool[] = proc_open("php yii $task->route", [], $pipe);
        }

        // 回收子进程
        while (count($pool)) {
            foreach ($pool as $i => $result) {
                $etat = proc_get_status($result);
                if($etat['running'] == FALSE) {
                    proc_close($result);
                    unset($pool[$i]);
                    # 记录任务状态
                    $tasks[$i]->exectime     = round($this->getCurrentTime() - $startExectime, 2);
                    $tasks[$i]->last_rundate = date('Y-m-d H:i');
                    $tasks[$i]->next_rundate = $tasks[$i]->getNextRunDate();
                    $tasks[$i]->status       = 0;
                    // 任务出错
                    if ($etat['exitcode'] !== ExitCode::OK) {
                        $tasks[$i]->status = 1;
                    }   

                    $tasks[$i]->save(false);
                }
            }
        }
    }

    private function getCurrentTime ()  {  
        list ($msec, $sec) = explode(" ", microtime());  
        return (float)$msec + (float)$sec;  
    }
   
}

  • 实现crontab模型 common/models/Crontab.php 没有则自己创建
<?php
namespace app\common\models;

use Yii;
use app\common\helpers\CronParser;

/**
 * 定时任务模型
 * @author jlb
 */
class Crontab extends \yii\db\ActiveRecord
{

	/**
	 * switch字段的文字映射
	 * @var array
	 */
	private $switchTextMap = [
		0 => '关闭',
		1 => '开启',
	];

	/**
	 * status字段的文字映射
	 * @var array
	 */
	private $statusTextMap = [
		0 => '正常',
		1 => '任务保存',
	];

    public static function getDb()
    {
       #注意!!!替换成自己的数据库配置组件名称
        return Yii::$app->tfbmall;
    }
    /**
     * 获取switch字段对应的文字
     * @author jlb
     * @return ''|string
     */
    public function getSwitchText()
    {
    	if(!isset($this->switchTextMap[$this->switch])) {
    		return '';
    	}
    	return $this->switchTextMap[$this->switch];
    }

    /**
     * 获取status字段对应的文字
     * @author jlb
     * @return ''|string
     */
    public function getStatusText()
    {
    	if(!isset($this->statusTextMap[$this->status])) {
    		return '';
    	}
    	return $this->statusTextMap[$this->status];
    }

    /**
     * 计算下次运行时间
     * @author jlb
     */
    public function getNextRunDate()
    {
    	if (!CronParser::check($this->crontab_str)) {
    		throw new \Exception("格式校验失败: {$this->crontab_str}", 1);
    	}
    	return CronParser::formatToDate($this->crontab_str, 1)[0];
    }

}
  • 一个crontab格式工具解析类common/helpers/CronParser.php
<?php
namespace app\common\helpers;

/**
 * crontab格式解析工具类
 * @author jlb <497012571@qq.com>
 */
class CronParser
{

    protected static $weekMap = [
        0 => 'Sunday',
        1 => 'Monday',
        2 => 'Tuesday',
        3 => 'Wednesday',
        4 => 'Thursday',
        5 => 'Friday',
        6 => 'Saturday',
    ];

    /**
     * 检查crontab格式是否支持
     * @param  string $cronstr 
     * @return boolean true|false
     */
    public static function check($cronstr)
    {
        $cronstr = trim($cronstr);

        if (count(preg_split('#\s+#', $cronstr)) !== 5) {
            return false;
        }

        $reg = '#^(\*(/\d+)?|\d+([,\d\-]+)?)\s+(\*(/\d+)?|\d+([,\d\-]+)?)\s+(\*(/\d+)?|\d+([,\d\-]+)?)\s+(\*(/\d+)?|\d+([,\d\-]+)?)\s+(\*(/\d+)?|\d+([,\d\-]+)?)$#';
        if (!preg_match($reg, $cronstr)) {
            return false;
        }

        return true;
    }

    /**
     * 格式化crontab格式字符串
     * @param  string $cronstr
     * @param  interge $maxSize 设置返回符合条件的时间数量, 默认为1
     * @return array 返回符合格式的时间
     */
    public static function formatToDate($cronstr, $maxSize = 1) 
    {

        if (!static::check($cronstr)) {
            throw new \Exception("格式错误: $cronstr", 1);
        }

        $tags = preg_split('#\s+#', $cronstr);

        $crons = [
            'minutes' => static::parseTag($tags[0], 0, 59), //分钟
            'hours'   => static::parseTag($tags[1], 0, 23), //小时
            'day'     => static::parseTag($tags[2], 1, 31), //一个月中的第几天
            'month'   => static::parseTag($tags[3], 1, 12), //月份
            'week'    => static::parseTag($tags[4], 0, 6), // 星期
        ];

        $crons['week'] = array_map(function($item){
            return static::$weekMap[$item];
        }, $crons['week']);

        $nowtime = strtotime(date('Y-m-d H:i'));
        $today = getdate();
        $dates = [];
        foreach ($crons['month'] as $month) {
            // 获取单月最大天数
            $maxDay = cal_days_in_month(CAL_GREGORIAN, $month, date('Y'));
            foreach ($crons['day'] as $day) {
                if ($day > $maxDay) {
                    break;
                }
                foreach ($crons['hours'] as $hours) {
                    foreach ($crons['minutes'] as $minutes) {
                        $i = mktime($hours, $minutes, 0, $month, $day);
                        if ($nowtime > $i) {
                            continue;
                        }
                        $date = getdate($i);

                        // 解析是第几天
                        if ($tags[2] != '*' && in_array($date['mday'], $crons['day'])) {
                            $dates[] = date('Y-m-d H:i', $i);
                        }

                        // 解析星期几
                        if ($tags[4] != '*' && in_array($date['weekday'], $crons['week'])) {
                            $dates[] = date('Y-m-d H:i', $i);
                        }

                        // 天与星期几
                        if ($tags[2] == '*' && $tags[4] == '*') {
                            $dates[] = date('Y-m-d H:i', $i);
                        }

                        
                        if (isset($dates) && count($dates) == $maxSize) {
                            break 4;
                        }
                    }
                }
            }
        }

        return array_unique($dates);
    }
    /**
     * 解析元素
     * @param  string $tag  元素标签
     * @param  integer $tmin 最小值
     * @param  integer $tmax 最大值
     * @throws \Exception
     */
    protected static function parseTag($tag, $tmin, $tmax)
    {
        if ($tag == '*') {
            return range($tmin, $tmax);
        }

        $step = 1;
        $dateList = [];

        if (false !== strpos($tag, '/')) {
            $tmp = explode('/', $tag);
            $step = isset($tmp[1]) ? $tmp[1] : 1;
            
            $dateList = range($tmin, $tmax, $step);
        }
        else if (false !== strpos($tag, '-')) {
            list($min, $max) = explode('-', $tag);
            if ($min > $max) {
                list($min, $max) = [$max, $min];
            }
            $dateList = range($min, $max, $step);
        }
        else if (false !== strpos($tag, ',')) {
            $dateList = explode(',', $tag);
        }
        else {
            $dateList = array($tag);
        }

        // 越界判断
        foreach ($dateList as $num) {
            if ($num < $tmin || $num > $tmax) {
                throw new \Exception('数值越界');
            }
        }

        sort($dateList);

        return $dateList;
    }
}

大功告成

创建一个用于测试的方法吧 commands/tasks/TestController.php

<?php
namespace app\commands\tasks;

use Yii;
use yii\console\Controller;
use yii\console\ExitCode;

class TestController extends Controller
{
    /**
     * @return int Exit code
     */
    public function actionIndex()
    {
		sleep(1);
        echo "我是index方法\n";
        return ExitCode::OK;
    }

    /**
     * @return int Exit code
     */
    public function actionTest()
    {
		sleep(2);
        echo "我是test方法\n";
        return ExitCode::OK;
    }

}

还记得一开始就创建好的crontab表吗,手动在表添加任务如下
image.png

进入yii根目录运行 php yii crontab/index即可看到效果

最后祭出我做好的的增删改查定时任务管理界面

这一块就劳烦你自己动动手仿照做出来吧

image.png

 

用crontab 一分钟运行一次

* * * * * cd /yii-project/ && php yii crontab/index

旧的CronParser类不完善有BUG,所以附上最新的 crontab解析类
大家也许发现了,我这种方案只支持单服务器部署,如果定时任务太多,单机不够的情况下要做下集群,我也是有个方案,但是还没实际运用,是否有必要提上来,需要看大家的反馈与需求

 
 
G
M
T
 
 
Detect languageAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBengaliBosnianBulgarianCatalanCebuanoChichewaChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEsperantoEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekGujaratiHaitian CreoleHausaHebrewHindiHmongHungarianIcelandicIgboIndonesianIrishItalianJapaneseJavaneseKannadaKazakhKhmerKoreanLaoLatinLatvianLithuanianMacedonianMalagasyMalayMalayalamMalteseMaoriMarathiMongolianMyanmar (Burmese)NepaliNorwegianPersianPolishPortuguesePunjabiRomanianRussianSerbianSesothoSinhalaSlovakSlovenianSomaliSpanishSundaneseSwahiliSwedishTajikTamilTeluguThaiTurkishUkrainianUrduUzbekVietnameseWelshYiddishYorubaZulu
 
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBengaliBosnianBulgarianCatalanCebuanoChichewaChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEsperantoEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekGujaratiHaitian CreoleHausaHebrewHindiHmongHungarianIcelandicIgboIndonesianIrishItalianJapaneseJavaneseKannadaKazakhKhmerKoreanLaoLatinLatvianLithuanianMacedonianMalagasyMalayMalayalamMalteseMaoriMarathiMongolianMyanmar (Burmese)NepaliNorwegianPersianPolishPortuguesePunjabiRomanianRussianSerbianSesothoSinhalaSlovakSlovenianSomaliSpanishSundaneseSwahiliSwedishTajikTamilTeluguThaiTurkishUkrainianUrduUzbekVietnameseWelshYiddishYorubaZulu
 
 
 
 
 
 
 
 
 
Text-to-speech function is limited to 200 characters
 
 
Options : History : Feedback : Donate Close

© 著作权归作者所有

botkenni
粉丝 20
博文 429
码字总数 444521
作品 0
西城
程序员
私信 提问
liufee/yii2-swoole

yii2 swoole 让yii2运行在swoole上。如果您在使用中遇到问题或者想学习yii2结合swoole可以加qq群258780872一起讨论 性能 运行在swoole上的yii2是运行在php-fpm上yii2的5倍以上,而且一句代码...

liufee
2017/12/26
0
0
yii2项目实战-博客管理平台的搭建

作者:白狼 出处:http://www.manks.top/document/yii2-blog-manage.html 本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律...

白狼栈
2016/08/13
162
0
基于 yii2 的内容管理系统--FeehiCMS

FeehiCMS是基于yii2的CMS系统,运行环境与yii2(php>=5.4)一致。FeehiCMS旨在为yii2爱好者提供一个基础功能完善的CMS系统,使开发者更专注于业务功能开发。 FeehiCMS没有对yii2做任何的修改、...

liufee
2017/11/22
6K
4
【翻译】Yii2 第1章 开始

让我们看看,怎样以最小的代价使用Yii2创建一个站点。目的是学习使用Yii2应用模版的安装过程,并开始体验模版里提供的一系列特性。 一个基本应用 开始使用Yii2最基本和直接的方式,是使用Yii...

zcgly
2015/09/15
964
7
基于 swoole 的 job 调度组件 - swoole-jobs

swoole-jobs 基于swoole类似gearman的分布式任务处理系统 高性能/动态多woker进程消费队列,加速后端耗时服 无需像gearman一个worker配置一条crontab,swoole-jobs负责管理所有worker状态 独...

peigreet
2017/11/29
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

UAVStack功能上新:新增JVM监控分析工具

UAVStack推出的JVM监控分析工具提供基于页面的展现方式,以图形化的方式展示采集到的监控数据;同时提供JVM基本参数获取、内存dump、线程分析、内存分配采样和热点方法分析等功能。 引言 作为...

宜信技术学院
18分钟前
4
0
MySQL的5种时间类型的比较

日期时间类型 占用空间 日期格式 最小值 最大值 零值表示 DATETIME 8 bytes YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00 9999-12-31 23:59:59 0000-00-00 00:00:00 TIMESTAMP 4 bytes YYYY-MM......

物种起源-达尔文
25分钟前
6
0
云服务OpenAPI的7大挑战,架构师如何应对?

阿里妹导读:API 是模块或者子系统之间交互的接口定义。好的系统架构离不开好的 API 设计,而一个设计不够完善的 API 则注定会导致系统的后续发展和维护非常困难。比较好的API设计样板可以参...

阿里云官方博客
28分钟前
5
0
Rancher + VMware PKS实现全球数百站点的边缘K8S集群管理

Sovereign Systems是一家成立于2007年的技术咨询公司,帮助客户将传统数据中心技术和应用程序转换为更高效的、基于云的技术平台,以更好地应对业务挑战。曾连续3年提名CRN,并且在2012年到2...

RancherLabs
33分钟前
5
0
6、根据坐标,判断该坐标是否在地图区域范围内

最近在写配送区域相关的代码,具体需求如下: 根据腾讯地图划分配送区域,总站下边设多个配送分站,然后将订单中的收货地址将其分配给不同的配送分站。 1、地图区域划分(腾讯地图) 1.1、H...

有一个小阿飞
35分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部