前面都是解决问题的过程,如果只需要最终解决方案可直接跳到本文最下方。
前段时间需要做个带有每日积分转换代金券的项目,其核心功能是每日定时将所有用户的积分按比例转换成代金券额。本来不是什么难事,swoole、CronTab等一堆解决方案,但客户用的是非自有服务器(即虚拟主机,由服务器商托管,无远程桌面,无php.ini修改权限,只有FTP),所以只能通过纯PHP代码来实现。
最开始网上找的方案是用pcntl_alarm()函数,但虚拟主机本身环境不支持pcntl扩展,也没有新的扩展权限,只能继续再找。
然后找到有用while()+sleep()的方式,具体代码如下:
ignore_user_abort();//关掉浏览器后脚本继续执行
set_time_limit(0);//设置脚本执行时间无上限
ini_set('memory_limit','512M');//内存限制设置
$interval_time = 10;//间隔为10秒
do{
/*
*业务逻辑
*/
sleep($interval_time);//程序暂时睡眠
}while(true);
初期测试并没有什么问题,但时间一长就有很大概率程序中断,不能持续稳定的执行定时显然不行。
在参考过一些网上资料与本地调试后,找到一个相较于前一个方法稳定的方式,使用采集函数file_get_contents()代替while()循环,具体代码如下:
ignore_user_abort();//关掉浏览器后脚本继续执行
set_time_limit(0);//设置脚本执行时间无上限
ini_set('memory_limit','512M');//内存限制设置
$interval_time = 10;//间隔为10秒
/*
*业务逻辑
*/
sleep($interval_time);//程序暂时睡眠
file_get_contents(demo.php)//采集本身,demo.php为本文件名
还是一样,初期测试并没有什么问题,但后续出现两个问题:一是业务逻辑运行本身需要时间,单次耗时可能不算什么,但一旦累计好几天很可能就会造成时间大偏差,例如原本预计晚上12点运行的,经过几次定时任务后可能就会推后到1点甚至2点才运行。二是成功运行几次后(几天后),程序还是有可能被中断。
经过再次的参考网上资料与本地调试后,发现结合MySql(换成Memcache或Redis也行,思路不变)使用可以解决问题,具体代码如下:
<?php
require(dirname(__FILE__)."/load.php");//load.php里包含了一些连接数据库的函数,例如执行SQL语句的函数sqlone()
ignore_user_abort();//关掉浏览器后脚本继续执行
set_time_limit(0);//设置脚本执行时间无上限
ini_set('memory_limit','512M');//内存限制设置
$interval_time = 86400;//循环执行时间间隔
$url="http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];//获取本页面URL
$now = time();//现在的时间戳
$everday = sqlone("select * from `everday` order by id desc limit 1","查询最后一次执行循环任务的信息");
if(!file_exists(dirname(__FILE__).'/sql_ecex_'.date("d",strtotime($everday['logic_date'])))){exit;}//验证日期文件夹名(作用:过滤多余执行脚本)
if(!$everday || !$everday['exec_time']){//如果没有执行过循环任务
exec_everday();//则立即执行一次
}else{
$sleep = $now-strtotime($everday['logic_date']);//计算出距上次执行时间已经过了多少秒
if($sleep<$interval_time){//如果还没超过时间间隔
sleep($interval_time-$sleep+rand(0,600));//则休眠对应时间(加上随机数延迟,防止并发)
}else{//如果已超过时间间隔
while($sleep>=$interval_time){
$sleep-=$interval_time;
exec_everday($sleep);//则执行一次或多次
}
}
}
function exec_everday($interval=0){
global $now;
$logic_date = $now-$interval;//逻辑时间戳=现在-时间差
rename("sql_ecex_".date("d",strtotime("-1 day",$logic_date)),"sql_ecex_".date("d",$logic_date));//更改日期文件夹名
sqlone("INSERT INTO `everday` VALUES (NULL,'".$now."','".date("Y-m-d H:i:s",$now)."','".date("Y-m-d H:i:s",$logic_date)."','0','');","插入定时任务记录");
/*
*业务逻辑
*/
}
file_get_contents($url);//重新加载
以上脚本程序如果需要正常运行,需要在同目录新建一个名为“sql_ecex_下次执行日期”的文件夹,例如下次执行是在是3月20日晚上11点30,那么就新建一个sql_ecex_20的文件夹,如果想中断下次循环,只需删除该文件夹即可。其中执行时间与逻辑时间分别指的是:程序实际执行任务的时间和程序本应该执行任务的时间。
最终,我们将其简化,让其适用性不再局限每日,并且用文件记录的方式代替MySql来实现,具体代码如下:
<?php
ignore_user_abort();//关掉浏览器后脚本继续执行
set_time_limit(0);//设置脚本执行时间无上限
ini_set('memory_limit','512M');//内存限制设置
$interval_time = 86400;//循环执行时间间隔
$url="http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];//获取本页面URL
$now = time();//现在的时间戳
$everdaytime = file_exists("everdaytime.txt")?file_get_contents("everdaytime.txt"):"";//上次任务执行实际时间
$logictime = file_exists("logictime.txt")?file_get_contents("logictime.txt"):"";//上次任务执行逻辑时间
if(!$everdaytime){//如果没有执行过循环任务
exec_everday();//则立即执行一次
}else{
$sleep = $now-strtotime($logictime);//计算出距上次逻辑执行时间已经过了多少秒
if($sleep<$interval_time){//如果还没超过时间间隔
sleep($interval_time-$sleep+rand(0,600));//则休眠对应时间(加上随机数延迟,防止误操作执行了多次导致并发)
$logictime_now = file_exists("logictime.txt")?file_get_contents("logictime.txt"):"";//休眠后的逻辑时间
if($logictime!=$logictime_now){exit;}//如果两个逻辑时间不相等,则说明休眠时间段内有程序执行过,本程序为多余程序,遂过滤掉
}else{//如果已超过时间间隔
while($sleep>=$interval_time){
$sleep-=$interval_time;
exec_everday($sleep);//则执行一次或多次
}
}
}
/*
* 执行每日定时任务与写入执行记录
* $interval距上次逻辑执行时间的时间差
*/
function exec_everday($interval=0){
global $now;//获取当前时间戳
$logic_date = $now-$interval;//逻辑时间戳=现在-时间差
file_put_contents("everdaytime.txt",date("Y-m-d H:i:s",$now));//写入本次实际执行时间
file_put_contents("logictime.txt",date("Y-m-d H:i:s",$logic_date));//写入本次逻辑执行时间
//业务逻辑...
}
file_get_contents($url);//循环下一次执行本程序
exit;//删除本文件后程序将停止运行
定时任务每执行过一次后会在同目录新建或替换两个文件,everdaytime.txt记录上次任务实际执行时间,logictime.txt记录上次任务本应该执行时间。将两个文件删除后就不会执行下次任务了,也就达到了终止的目的。
举一反三:两个执行时间其实可以用GET参数传值,最后file_get_contents也可以换成include。
如需转载请注明出处:https://blog.csdn.net/zhouchang224/article/details/88706689