構建一個定時任務管理後臺

構建一個定時任務管理後臺

實現一個調度定時任務的功能,不需要每次在linux 鍾每次要做定時任務時就要在linux上註冊任務(掛任務)。

實現如下:

創建一個專門管理定時任務的表


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運行中 2任務報錯',
  `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`)
) 

crontab表的status字段,巧用樂觀鎖。

實現原理: 當任務判斷爲可以運行時就把就把定時任務狀態調用update方法,改爲1, 成功rue,失敗false, 只有true的任務才能執行,反之跳過

說一下自己遇到的一些坑, 例如服務器斷電或者掛了, 那麼就會導致一些進行中的任務狀態沒改成正常, 所以,斷電了就要把定時任務的狀態都恢復成正常

所有任務通過一個入口方法來調度


* * * * * 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;
    }
    
    private function executeTask(array $tasks)
    {
        if (empty($tasks)) {
            return;
        }

        shuffle($tasks);

        $pool = [];
        $startExectime = $this->getCurrentTime();
        $pid = getmypid();
        $startDate = date('Y-m-d H:i:s');

        foreach ($tasks as $k => $task) {
            // 樂觀鎖, 上鎖. 防止同一任務,重複執行, 
            $task->status = 1;
            $isOk = $task->update(false); 
            if ($isOk) {
                Yii::info("pid[$pid] 定時任務啓動: {$task->name}, route: {$task->route}");
                $pool[$k] = proc_open("php yii $task->route", [], $pipe);
            }
        }

        // 防止同一任務,在多個機器上,重複執行
        sleep(self::WAIT_SYNC_TIME);

        // 回收子進程
        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 - self::WAIT_SYNC_TIME, 2);
                    $tasks[$i]->last_rundate = $startDate;
                    $tasks[$i]->next_rundate = $tasks[$i]->getNextRunDate();
                    $tasks[$i]->status       = '0';

                    // 任務出錯
                    if ($etat['exitcode'] !== ExitCode::OK) {
                        $tasks[$i]->status = 2;
                    }
                    $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
{
     const WAIT_SYNC_TIME = 5; //定義這個全局量,是爲了保證所有服務器的服務器時間一致
    

	/**
	 * 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 <[email protected]>
 */
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表嗎,手動在表添加任務如下
在這裏插入圖片描述

進入yii根目錄運行 php yii crontab/index即可看到效果.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章