定时任务

    目录

    说明

    在实际项目中,我们经常会有一些任务是需要定时执行的。

    虽然有 croncrontabsystemd 等系统级内置的,定时任务工具存在。

    但是他们的一些让人掉头发的配置写法,以及增加运维心智负担,无法适应多实例部署场景等等原因,势必需要在 imi 框架中提供这个功能。

    设计

    imi 通过增加一个 CronProcess 进程用于定时任务的调度和执行,使用 Redis 作为数据存储。

    定时任务支持在以下进程中执行: Task 进程、Worker 进程,也支持新运行一个 Process 进程。

    支持设置某进程在当前实例/多实例中只运行一个。

    使用

    启用定时任务进程

    必须在项目 config.phpbeans 中加入配置启用定时任务进程,否则所有定时任务都无法生效。

    配置代码:

    'AutoRunProcessManager'   =>  [
        'processes' =>  [
            'CronProcess',
        ],
    ],

    定义任务

    协程任务

    实现 Imi\Cron\Contract\ICronTask 接口、run() 方法,无需手动上报任务完成。

    <?php
    namespace Imi\Test\HttpServer\Cron;
    
    use Imi\Cron\Annotation\Cron;
    use Imi\Cron\Contract\ICronTask;
    
    /**
     * @Cron(id="CronRandomWorker", second="3n", type="random_worker")
     */
    class CronRandomWorker implements ICronTask
    {
        /**
         * 执行任务
         *
         * @param string $id
         * @param mixed $data
         * @return void
         */
        public function run(string $id, $data): void
        {
            var_dump('random');
        }
    
    }

    Task 任务

    使用task定时任务时,需要在项目config.php中的服务器配置里,开启task_worker_num参数,否则会报下面的错:

    Uncaught ErrorException: Swoole\Server::task(): task method can't be executed without task worker

    // 主服务器配置
    'mainServer'    =>    [
        'namespace'    =>    'ImiApp\ApiServer',
        'type'        =>    Imi\Server\Type::HTTP,
        'host'        =>    '0.0.0.0',
        'port'        =>    9501,
        'configs'    =>    [
            // 'worker_num'        =>  8,
            'task_worker_num'   =>  16, // 必须开启这个参数,否则报错
        ],
    ],

    与异步任务写法基本一致,多了@Cron注解,并且需要上报任务完成

    <?php
    namespace Imi\Test\HttpServer\Cron;
    
    use Imi\Swoole\Task\TaskParam;
    use Imi\Cron\Annotation\Cron;
    use Imi\Swoole\Task\Annotation\Task;
    use Imi\Cron\Util\CronUtil;
    use Imi\Swoole\Task\Interfaces\ITaskHandler;
    
    /**
     * @Cron(id="TaskCron", second="3n", data={"id":"TaskCron"})
     * @Task("CronTask1")
     */
    class TaskCron implements ITaskHandler
    {
        /**
         * 任务处理方法.
         *
         * @return mixed
         */
        public function handle(TaskParam $param, \Swoole\Server $server, int $taskId, int $workerId)
        {
            // 上报任务完成
            CronUtil::reportCronResult($param->getData()['id'], true, '');
            return date('Y-m-d H:i:s');
        }
    
        /**
         * 任务结束时触发.
         *
         * @param mixed $data
         */
        public function finish(\Swoole\Server $server, int $taskId, $data): void
        {
        }
    
    }

    Process 任务

    与进程写法基本一致,多了@Cron注解,并且需要上报任务完成

    <?php
    namespace Imi\Test\HttpServer\Cron;
    
    use Imi\Cli\ImiCommand;
    use Imi\Util\Args;
    use Imi\Swoole\Process\Contract\IProcess;
    use Imi\Cron\Annotation\Cron;
    use Imi\Cron\Util\CronUtil;
    use Imi\Swoole\Process\Annotation\Process;
    use Swoole\Event;
    
    /**
     * @Cron(id="CronProcess1", second="3n")
     * @Process("CronProcess1")
     */
    class TaskProcess implements IProcess
    {
        public function run(\Swoole\Process $process): void
        {
            $success = false;
            $message = '';
            // 获取任务 ID
            $input = ImiCommand::getInput();
            $id = $input->getParameterOption('--id');
            if (false === $id)
            {
                return;
            }
            try
            {
                // 做一些事情
                // ...
    
                $success = true;
            }
            catch (\Throwable $th)
            {
                $message = $th->getMessage();
                throw $th;
            }
            finally
            {
                // 上报任务完成
                CronUtil::reportCronResult($id, $success, $message);
            }
        }
    
    }

    定时规则

    支持注解设定和配置文件设定两种模式,其中配置文件设定,是可以覆盖注解设定的。

    注解设定

    注解 @Cron,类 Imi\Cron\Annotation\Cron

    @Cron(id="任务唯一ID", type="", year="", month="", day="", hour="", minute="", second="", unique=null, redisPool="", lockWaitTimeout="", maxExecutionTime="", force=false, delayMin=0, delayMax=0, successLog=true)

    属性

    id:

    使用@Cron注解时的任务唯一ID。如果是 TaskProcess,默认使用 TaskProcess + 名称。

    type:

    任务类型

    可选:

    random_worker-随机工作进程任务

    all_worker-所有工作进程执行的任务

    task-后台任务

    process-进程

    cron_process-定时任务进程

    force:

    每次启动服务强制执行,默认为false

    year:

    指定任务执行年份,默认为 *

    * - 不限制

    2019 - 指定年

    2019-2022 - 指定年份区间

    2019,2021,2022 - 指定多个年份

    2n - 每 2 年,其它以此类推

    取值范围:>=1 and <= 2100,不在范围内会抛出异常。
    在 imi 3.0 中会考虑移除最大到 2100 年的限制。(不过真的有项目能跑到那个时候吗?)

    month:

    指定任务执行月份,默认为 *

    * - 不限制

    1 (1 月), -1 (12 月) - 指定月份,支持负数为倒数的月

    1-6 (1-6 月), -3--1 (10-12 月) - 指定月份区间,支持负数为倒数的月

    1,3,5,-1 (1、3、5、12 月) - 指定多个月份,支持负数为倒数的月

    2n - 每 2 个月,其它以此类推

    取值范围:>=1 and <= 12,不在范围内会抛出异常。

    day:

    指定任务执行日期,默认为 *

    * - 不限制

    1 (1 日), -1 (每月最后一天) - 指定日期,支持负数为倒数的日期

    1-6 (1-6 日), -3--1 (每月倒数 3 天) - 指定日期区间,支持负数为倒数的日期

    1,3,5,-1 (每月 1、3、5、最后一天) - 指定多个日期,支持负数为倒数的日期

    2n - 每 2 天,其它以此类推

    year 1 (一年中的第 1 日), year -1 (每年最后一天) - 指定一年中的日期,支持负数为倒数的日期

    1-6 (一年中的第 1-6 日), -3--1 (每年倒数 3 天) - 指定一年中的日期区间,支持负数为倒数的日期

    year 1-6 (一年中的第 1-6 日), year -3--1 (每年倒数 3 天) - 指定一年中的日期区间,支持负数为倒数的日期

    1,3,5,-1 (每年 1、3、5、最后一天) - 指定一年中的多个日期,支持负数为倒数的日期

    year 1,3,5,-1 (每年 1、3、5、最后一天) - 指定一年中的多个日期,支持负数为倒数的日期

    取值范围:>=1 and <= 31,不在范围内会抛出异常。
    year 的取值范围:>=1 and <= 366,不在范围内会抛出异常。

    week:

    指定周几执行任务,默认为 *
    * - 不限制
    1 (周一), -1 (周日) - 指定周几(1-7),支持负数为倒数的周
    1-6 (周一到周六), -3--1 (周五到周日) - 指定周几,支持负数为倒数的周
    1,3,5,-1 (周一、三、五、日) - 指定多个日期,支持负数为倒数的周

    取值范围:>=1 and <= 7,不在范围内会抛出异常。

    hour:

    指定任务执行小时,默认为 *

    * - 不限制

    0 (0 点), -1 (23 点) - 指定小时,支持负数为倒数的小时

    1-6 (1-6 店), -3--1 (21-23 点) - 指定小时区间,支持负数为倒数的小时

    1,3,5,-1 (1、3、5、23 点) - 指定多个小时,支持负数为倒数的小时

    2n - 每 2 小时,其它以此类推

    取值范围:>=0 and <= 23,不在范围内会抛出异常。

    minute:

    指定任务执行分钟,默认为 *

    * - 不限制

    0 (0 分), -1 (59 分) - 指定分钟,支持负数为倒数的分钟

    1-6 (1-6 分), -3--1 (57-59 分) - 指定分钟区间,支持负数为倒数的分钟

    1,3,5,-1 (1、3、5、59 分) - 指定多个分钟,支持负数为倒数的分钟

    2n - 每 2 分钟,其它以此类推

    取值范围:>=0 and <= 59,不在范围内会抛出异常。

    second:

    指定任务执行秒,默认为 *

    * - 不限制

    0 (0 秒), -1 (59 秒) - 指定秒,支持负数为倒数的秒

    1-6 (1-6 秒), -3--1 (57-59 秒) - 指定秒区间,支持负数为倒数的秒

    1,3,5,-1 (1、3、5、59 秒) - 指定多个秒,支持负数为倒数的秒

    2n - 每 2 秒,其它以此类推

    取值范围:>=0 and <= 59,不在范围内会抛出异常。

    unique:

    定时任务唯一性设置
    当前实例唯一: current
    所有实例唯一: all
    不唯一: null

    redisPool:

    用于锁的 Redis 连接池名

    lockWaitTimeout:

    获取锁超时时间,单位:秒

    maxExecutionTime:

    最大运行执行时间,单位:秒。

    该值与分布式锁超时时间共享,默认为 60 秒

    delayMin、delayMax:

    最小、最大延迟执行秒数,默认为0

    如果有一项不为0,该定时任务就会根据两个值之间的随机秒数(包含两个值),提前或者延后执行。

    这两个设置主要是防止固定时间执行任务过多,起到分流作用。

    successLog:

    是否记录成功日志,默认为 true

    配置文件设定

    项目配置文件,beans 节中配置

    [
        'CronManager'   =>  [
            // 启用任务进程终端输出
            'stdOutput' => true,
            // 任务列表定义
            'tasks' =>  [
                // 任务唯一ID
                'taskName'  =>  [
                    // 任务类型,可选:worker-工作进程任务; task-任务; process-进程
                    'type'      =>  'worker',
                    // 任务执行回调,可以是callable类型,也可以是 task、process 名
                    'task'      =>  mixed,
                    // 定时配置
                    'cron'     =>  [
                        // 支持多个条件去触发
                        // 规则同 @Cron 注解
                        [
                            'year'  =>  '',
                            'month' =>  '',
                            'day'   =>  '',
                            'week'  =>  '',
                            'hour'  =>  '',
                            'minute'=>  '',
                            'second'=>  '',
                        ],
                    ],
                    // 可选配置
                    'data'              =>  null,
                    'unique'            =>  null,
                    'redisPool'         =>  'redis',
                    'lockWaitTimeout'   =>  10,
                    'maxExecutionTime'  =>  120,
                    'force'             =>  false,
                ],
            ],
        ],
    ]

    动态维护

    增加定时任务

    use Imi\Cron\Util\CronUtil;
    use Imi\Cron\Annotation\Cron;
    use Imi\Test\HttpServer\Cron\CronDWorker;
    
    $cron = new Cron;
    $cron->id = 'CronRandomWorkerTest';
    $cron->second = '3n';
    $cron->type = 'random_worker';
    CronUtil::addCron($cron, CronDWorker::class);

    移除定时任务

    use Imi\Cron\Util\CronUtil;
    
    CronUtil::removeCron('任务ID');

    移除所有任务

    use Imi\Cron\Util\CronUtil;
    
    CronUtil::clear();

    检测是否存在任务

    use Imi\Cron\Util\CronUtil;
    
    $hasTasks = CronUtil::hasTask('任务ID');
    echo "任务ID是否存在: $hasTasks";

    获取单个任务

    use Imi\Cron\Util\CronUtil;
    
    $task = CronUtil::getTask('任务ID');
    echo "任务#$task->id : $task->task";

    注:TaskMsg对象属性如下:

    参数类型说明
    idstring任务ID
    typestring任务类型
    taskstring任务类
    cronRulesarray[CronRuleOjbect]运行规则
    dataarray运行参数
    unique['current','all',null]唯一性设置
    redisPoolstring锁连接池名
    lockWaitTimeoutfloat锁超时时间
    maxExecutionTimefloat最大运行执行时间
    lastRunTimeint最近执行时间戳
    forcebool是否启动服务强制执行

    获取所有任务

    use Imi\Cron\Util\CronUtil;
    
    $realTasks = CronUtil::getRealTasks();
    
    foreach ($realTasks as $taskName => $task) {
        echo "任务#$taskName : $task->id";
    }