ThinkPHP定时任务(二)

January 23, 2017

前面说了TP框架中定时任务实现,也对存在的问题和优势做了一个简单的分析。如果有兴趣看的朋友可以参看ThinkPHP定时任务(一)。这篇主要对基于传统定时任务crontab[守护进程],外加TP框架支持实现的基于TP的定时任务

动机

用过TP的朋友都知道,TP框架是一个由国人开发的很优秀且轻量的PHP Web框架。由于其丰富的文档和强大的用户群体,在国内有不错的保有量。记得之前项目之初选择框架的时候,朋友建议选择TP,给的建议就是用户群体大,便于招人。
TP在Web模式下获得除了不俗的成绩,但是在非CGI模式下,通常(CLI),缺罕有人用。究其原因,个人觉得,无外乎以下几点:

  • 文档解释的比较少
  • 对CLI模式本身支持的比较简单
  • 有这样前后台执行需求的用户大多选择了laravel等国际框架

然而,我们的工程确实是用TP开发,为了保证代码结构统一,不用重复造轮子。就难免需要探索下TP框架下怎么实现后台任务的执行,如果这个问题解决了,再结合crontab就能找到我们想要的解决方案

TP框架的Mode

mode即是指应用模式,TP文档中是这么说明的:

应用模式提供了对核心框架进行改造的机会,可以让你的应用适应更多的环境和不同的要求。 每个应用模式有自己的模式定义文件,用于配置当前模式需要加载的核心文件和配置文件,以及别名定义、行为扩展定义等等。根据模式定义文件的定义位置和入口是否需要定义模式,可以分为显式应用模式和隐含应用模式。

而这里,我们要使用的就是TP3.2自带的api模式,要开启api模式,我们只需要做如下简单的配置

//cli模式
define('APP_MODE','api');

这个宏有默认值,如果我们没有定义的的话,会在应用的入口自动设置为我们的常用模式

defined('APP_MODE')     or define('APP_MODE',       'common'); // 应用模式 默认为普通模式   

关于api模式的配置文件,可以参考Think/mode/api.php,里边有对内置api应用模式的基础配置,我们可以根据我们的需求进行改写,或者通过配置覆写系统配置。而我们常用的普通模式就是该目录下的common.php,大家可以对比进行学习

兼容配置

APP_PATH 配置

由于我们的应用是在普通模式下工作的,当我们在调用API模式时,肯定希望应用目录保持一致,这样就不用做任何代码的迁移和改动。在api模式中加入如下配置

// 定义应用目录,同样指向common模式下的app目录
define('APP_PATH','../Apps/');

APP_STATUS 配置

起先,本人以为截止上一步就完成了所有配置。但是在调试的过程中,发现数据库无法连接,最终定位到。APPS目录中的配置没有正常加载。这个过程是个比较长的调试过程,具体就不表了。查看源代码发现,common模式下的所有配置是在Think/Libary/Think/Dispatcher.class.php中进行引入的,其中有如下代码,是完成MODULE中配置的加载的,有兴趣的同学可以自己看看

// 当前应用状态对应的配置文件
if(APP_STATUS && is_file(MODULE_PATH.'Conf/'.APP_STATUS.CONF_EXT))
    C(load_config(MODULE_PATH.'Conf/'.APP_STATUS.CONF_EXT));

    // 加载模块别名定义
    if(is_file(MODULE_PATH.'Conf/alias.php'))
        Think::addMap(include MODULE_PATH.'Conf/alias.php');
    // 加载模块tags文件定义
    if(is_file(MODULE_PATH.'Conf/tags.php'))
        Hook::import(include MODULE_PATH.'Conf/tags.php');
    // 加载模块函数文件
    if(is_file(MODULE_PATH.'Common/function.php'))
        include MODULE_PATH.'Common/function.php';
    // 加载模块的扩展配置文件
    load_ext_file(MODULE_PATH);

同样找到Think/Mode/Api/Dispatcher.class.php,在合适的时机添加上述加载代码(本人TP3.2是在146行开始加的),这样后就可加载MODULE中配置。
至此,所有的配置均已完成,下来我们来看看怎么操作。

目录结构

先看看我这边的目录结构吧,便于后续表述

    PROJECT
        - APPS
            + module_1
            + module_2
        - Crons
            + crons.conf
            + curl_runloop.sh
            + daemon.sh
            + exec.php
            + runloop.sh
            + start.sh

Crons就是我们的定时任务目录,这里我着重说一下Crons目录下各个文件的用途吧。

  • crons.conf 定时任务的配置文件,配置了要执行什么定时任务
  • curl_runloop.sh 通过url调用时的runloop脚本
  • daemon.sh 守护进程
  • runloop.sh 直接PHP调用的runloop脚本
  • exec.php命令执行脚本

如何执行

有了上面的铺垫,启动脚本其实很简单,如下:

php exec.php script_path

这里做个简单的说明,比如我们之前的访问的URL如下:

http://xxx.com/abc/controller/action

我们的script_path的值应该传:

abc/controller/action

这样即可调用的我们的对应方法,完成后台执行

时间控制

定时任务肯定逃不过计划执行时间,轮训时间。这里我从controller层做了些许限制。当然也可以在runloop中去做。将配置信息写在crons.conf中。进行配置既可
建立一个用于执行定时任务的基类来统一处理调度和并发逻辑。需要能别后台调用起的定时任务需要继承该基类,完成基础功能。主要涉及的一下几个地方。

// cronbaseclass 实现

// 多少时间执行一次
protected $interval = 60;

// 检测是否有权限调用
private function check() {
    if (PHP_SAPI != 'cli' && !in_array(get_client_ip(),C('CRONS_BIND_IP'))) {
        exit('what a fucking day!');
    }
}

// 入口方法
public function index() {
    set_time_limit(0);
    $exe_time = $this->cron_config(CONTROLLER_NAME.ACTION_NAME);
    if ($exe_time < time()) {
        $this->update_cron_config(CONTROLLER_NAME.ACTION_NAME,$this->interval + time());
        $lock_file = RUNTIME_PATH.md5(CONTROLLER_NAME.ACTION_NAME);
        if (empty(S($lock_file))) {
            S($lock_file,1,600);
            $this->run();
            S($lock_file,null);
        }
    }
}

// 子类逻辑执行方法
protected function run() {}

runloop

runloop 其实很简单,就是一个死循环,然后去读取配置文件,通过php exec.php xxx去执行脚本任务。特别需要注意的daemon.sh脚本,是我们统一守护进程。crontab中需要进行配置如下:

# 守护进程守护runloop.sh进程,去执行release环境[APP_STATUS]
* * * * * daemon.sh runloop.sh release

结语

到现在,我们所有的配置和执行方案已经分析完成。根据这些配置,大家可以灵活的组织各种定时情况。
其实每个框架都有自身的强大之处和不足,只要根据它的特性和设计方案出发,一般都能找到合适的方案。自己碰到的问题,相信框架的设计者也有所涉及。本文其实就是在TP的框架中做了一些小的改动来支持定时任务。

附录

crons.conf内容格式

Agreement/CCallback
Agreement/Achive
Agreement/SynchronizeStatus

Comments