構建一個定時任務管理後臺
實現一個調度定時任務的功能,不需要每次在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即可看到效果.