首先介绍一下我遇到过的,个人觉得奇葩的极其不方便的定时任务方式
每当有一个定时任务需求就在linux
下crontab
中注册一个任务
*/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"
不知道有不有大兄弟躺枪了,希望你看了我的实现方式后,以后不要这么搞定时任务了,当然我的也不会是最好了,别钻牛角尖
这种方式的定时任务有什么问题?
- 显而易见的就是不知道这种鬼链接是什么个东西,想停不敢停怕背锅,久而久之就扔上面
- http请求的方式触发任务,任务多的时候占用webserver的资源(
如果是以cli模式触发就算了,当我没说
) - 无法记录任务运行的状态,例如: 是否运行成功,运行一次耗时多少(
你千万别跟我说在每个任务记录个里日志啥的好吧
)
我将围绕如何解决以上三个问题来展开我的实现过程
- 创建一个专门管理定时任务的表
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 appcommands;
use Yii;
use yiiconsoleController;
use yiiconsoleExitCode;
use appcommonmodelsCrontab;
/**
* 定时任务调度控制器
* @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 appcommonmodels;
use Yii;
use appcommonhelpersCronParser;
/**
* 定时任务模型
* @author jlb
*/
class Crontab extends yiidbActiveRecord
{
/**
* 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 appcommonhelpers;
/**
* 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 appcommandstasks;
use Yii;
use yiiconsoleController;
use yiiconsoleExitCode;
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
即可看到效果
最后祭出我做好的的增删改查定时任务管理界面
这一块就劳烦你自己动动手仿照做出来吧
用crontab 一分钟运行一次
代码语言:javascript复制* * * * * 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 |
---|