ThinkPHP定时任务(一)

January 23, 2017

后台开发的过程中,免不了和定时任务打交道。每天定时的发短信给用户,定时的计算收益、利息,固定间隔的去执行某项大型任务(不适宜同步执行)等等,等等
常用的定时任务都是基于linux中的crontab进行,不过今天我们主要做TP框架内的定时任务实现解析,分析其实现原理和缺陷。在下一部分,会介绍一个新的方案

行为扩展

细心的朋友应该知道TP框架中有一个名叫行为扩展的东西。其实简单的概括起来就是HOOK,可以实现在某个时机进行HOOK,然后注册监听(Listen),实现代码和功能的无侵入式嵌入。行为扩展在我们的项目中使用的比较多,静态资源替换、错误模板替换(这个只有经历过才知道为什么)、自动初始化,太多的地方。总体来说,由于TP暴露了很多关键点的HOOK,我们在使用的时候,只需要去监听即可,所以使用起来还是很不错的。如果对行为扩展还有不了解的,可以去查看TP官方文档寻找答案

基于行为扩展的定时任务

没错,TP的定时任务既是建立在自身的行为扩展的基础上的,如果了解了行为扩展的朋友,应该知道行为扩展只是一种变相的代码调用,所以问题就随之而来,什么时候触发定时任务?我们先来看看在框架内应该以什么样的姿势来打开Crons
一般情况下,我们做如下初始化:

// Common/Conf/tags.php 文件
'app_begin'=>array(
    'Behavior\CronRunBehavior'
),

Common/Conf/tags.php 文件用于快速的注册行为扩展,跟HOOK有异曲同工之处。由于这个方法比较简洁方便,且对于框架代码无嵌入,值得使用

这个注册的意思是在程序启动的时候,直接调用Behavior\CronRunBehavior脚本,而Behavior\CronRunBehavior脚本的任务既是对定时任务的调用

时间间隔

做定时任务,肯定要关心调用频次,这里我们看下CronRunBehaviorController的实现代码来简单的分析下这个定时任务的实现逻辑
如下代码:

 // 锁定自动执行
 $lockfile    =   RUNTIME_PATH.'cron.lock';
 if(is_writable($lockfile) && filemtime($lockfile) > $_SERVER['REQUEST_TIME'] - C('CRON_MAX_TIME',null,60)) {
        return ;
 } else {
            touch($lockfile);
 }
 set_time_limit(1000);
 // 即使客户端断开连接,也要继续执行
 ignore_user_abort(true);

这段代码的意思是,在执行CronRunBehaviorController前,大概分为如下流程:

  1. 判断是否有锁文件,如果没有则创建锁文件,进行下一步,如有进行第2步
  2. 判断所文件的创建时间是否和当前时间间隔超过我们设置的时间间隔CRON_MAX_TIME,默认为60s,如果超过则进行下一步,反之不执行,直接退出

再看下面代码:

if(is_file(RUNTIME_PATH.'~crons.php')) {
    $crons  =   include RUNTIME_PATH.'~crons.php';
}elseif(is_file(COMMON_PATH.'Conf/crons.php')){
    $crons  =   include COMMON_PATH.'Conf/crons.php';
}

这块代码可以看出来我们的定时任务配置是在Conf/crons.php路径下,有runtime缓存,每次修改需要清除缓存,这里比较简单,我来贴上Conf/crons.php的配置即可

 return [
    'callback' => array('Agreement/Cron/call_back.php', 0.1),
 ];
  1. 其中callback是任务脚本名称,而对于的value则是任务脚本的配置
  2. 'Agreement/Cron/call_back.php'为具体执行脚本,依次为间隔时间,上次执行时间

至此,我们的基于TP的定时任务已经全部准备就绪。

写在最后

优势

  • 方便使用,如果是用TP实现的代码逻辑,全部逻辑都可以复用
  • 不用考虑实现具体调用细节,只需要按照规范来写就行
  • 所有的参数都比较直白,不用和crontab中的星星打交道
  • 对时效性不强的简单定时任务可以首选

缺点

  • 由实现逻辑可以看出,每次脚本的启动都是由客户触发,不能保证稳定的时间间隔
  • 配置时间不是很灵活(之前为了使用,也做了些许扩展)
  • 同步执行,很影响使用这的执行效率,如果任务脚本比较大,则会极度影响用户体验
  • 文件锁的方式虽然好用,但是对于分布式系统,无法做到锁功能

尝试过的改造

  • 对配置文件进行了升级,支持间隔时间的配置
  • 对执行方式进行了改造,支持基于pcntl的多进程调用,由于主进程需要先结束,为了实现逻辑最后造成进程混乱,不是很可取。
  • 使用redis对执行加锁,完成分布式锁

上述改造,由于第三点不好处理(也是最重要的一点),遂另辟蹊径,采用了另一种方式来处理。下一章节,我会对这个进行详细说明!

Comments