EasySwoole

Crontab

Crontab 用于在 EasySwoole 服务内部按照 Cron 表达式周期执行任务,例如清理过期数据、生成日报或同步业务状态。框架会在主服务启动时创建调度进程和任务 Worker,不需要另外配置 Linux crontab。

定义定时任务

新建 App/Crontab/ClearExpiredToken.php:

<?php

namespace App\Crontab;

use EasySwoole\Crontab\JobInterface;

class ClearExpiredToken implements JobInterface
{
    public function jobName(): string
    {
        return 'ClearExpiredToken';
    }

    public function crontabRule(): string
    {
        // 每 5 分钟执行一次。
        return '*/5 * * * *';
    }

    public function run(): mixed
    {
        // return TokenModel::query()
        //     ->where('expired_at', time(), '<')
        //     ->delete();

        return 0;
    }

    public function onException(\Throwable $throwable): void
    {
        // 记录异常或发送告警。
        throw $throwable;
    }
}

最新版 JobInterface::run() 的返回类型是 mixed,实现类应保持相同签名:

public function run(): mixed
{
    // 可以返回受影响行数、业务结果、布尔值或 null。
    return null;
}

返回值类型由具体任务决定;如果任务不需要返回结果,可以直接返回 null。任务执行失败时应抛出异常,让组件进入 onException(),不要只返回 false 后静默结束。

Cron 表达式包含五段:

分钟 小时 日期 月份 星期

例如 0 2 * * * 表示每天 02:00,*/10 * * * * 表示每 10 分钟。调度使用服务器时区,建议在 EasySwooleEvent::initialize() 中明确调用 date_default_timezone_set()。

注册任务

定时任务必须在服务完成创建前注册。编辑项目根目录的 EasySwooleEvent.php:

<?php

namespace EasySwoole\EasySwoole;

use App\Crontab\ClearExpiredToken;
use EasySwoole\EasySwoole\AbstractInterface\Event;
use EasySwoole\EasySwoole\Crontab\Crontab;
use EasySwoole\EasySwoole\Swoole\EventRegister;

class EasySwooleEvent implements Event
{
    public static function initialize()
    {
        date_default_timezone_set('Asia/Shanghai');
    }

    public static function mainServerCreate(EventRegister $register)
    {
        Crontab::getInstance()->register(new ClearExpiredToken());
    }
}

jobName() 在同一服务中必须唯一,重复注册会抛出异常。新增、删除 Job 或修改注册代码后需要完整重启服务。只修改 Job 在 run() 中延迟加载的独立业务类时,可以使用后文的 crontab reload 重建调度和执行进程。

普通任务与特权任务

普通任务实现 JobInterface,所有普通任务由同一个 CrontabWorker 进程接收并执行。特权任务实现 PrivilegeJobInterface;这个接口继承 JobInterface,没有增加新的方法。框架会为每个特权任务额外创建一个以任务名命名的独立 Worker,并将该任务的定时触发和 runJobNow 请求发往这个 Worker。

项目 普通任务 特权任务
实现接口 JobInterface PrivilegeJobInterface
执行进程 共用 CrontabWorker 每个任务有独立的 CrontabWorker.任务名
通信 Socket 共用普通任务 Socket 每个任务有专用 Socket
调度规则与管理命令 使用相同的 Cron 规则、show、stop、resume 等命令 相同
进程资源 任务之间共享一个 Worker 的进程资源 每个任务额外占用一个进程及其资源

这里的“特权”表示独立执行进程,不是 Linux root 权限,也不会改变进程用户、提高调度优先级或自动保证任务只运行一次。两类任务都由同一个 Scheduler 调度,执行 crontab reload 时都会被重建。

例如,报表任务使用独立进程可以避免它与普通清理任务共用 Worker 的内存和进程级状态。新建 App/Crontab/GenerateDailyReport.php:

<?php

namespace App\Crontab;

use EasySwoole\Crontab\PrivilegeJobInterface;

class GenerateDailyReport implements PrivilegeJobInterface
{
    public function jobName(): string
    {
        return 'GenerateDailyReport';
    }

    public function crontabRule(): string
    {
        return '0 2 * * *';
    }

    public function run(): mixed
    {
        // 将业务类留到执行进程中首次加载,便于发布后重载 Crontab 进程。
        return (new \App\Service\DailyReportService())->generate();
    }

    public function onException(\Throwable $throwable): void
    {
        throw $throwable;
    }
}

DailyReportService 是示例业务类,需要在项目中自行实现。然后在前面的 EasySwooleEvent::mainServerCreate() 中同时注册两个任务:

use App\Crontab\GenerateDailyReport;

Crontab::getInstance()->register(new ClearExpiredToken());
Crontab::getInstance()->register(new GenerateDailyReport());

服务重启后,进程结构类似:

EasySwoole.<mode>.CrontabScheduler                  调度全部任务
EasySwoole.<mode>.CrontabWorker                     执行普通任务
EasySwoole.<mode>.CrontabWorker.GenerateDailyReport  执行该特权任务

普通任务数量较多但资源消耗较轻时,先使用普通任务。只有某个任务需要独立的进程资源或与其他任务隔离时,再使用特权任务。专用 Worker 仍可能因任务自身执行过久、重复触发或多实例部署而产生并发与重复执行问题;需要业务锁、超时或幂等处理时仍须自行实现。

管理定时任务

服务启动后可以在项目根目录执行:

php easyswoole.php crontab show
php easyswoole.php crontab runJobNow --taskName=ClearExpiredToken
php easyswoole.php crontab stop --taskName=ClearExpiredToken
php easyswoole.php crontab resume --taskName=ClearExpiredToken
php easyswoole.php crontab setCrontabRule --taskName=ClearExpiredToken --taskRule='*/10 * * * *'
php easyswoole.php crontab reload

show 支持 --watch=2 持续刷新。stop、resume 和 runJobNow 省略 --taskName 时,会在交互式终端中打开多选界面;自动化脚本必须显式指定任务名。

非默认运行模式需要追加 --mode=produce。stop、resume 和 setCrontabRule 修改的是当前运行实例中的状态,服务重启或执行 crontab reload 后仍以启动时注册的 Job 配置为准。

crontab reload 会同时重建 Scheduler、普通 Worker 和特权任务 Worker。Job 类在 mainServerCreate() 注册时已经由 Master 加载,因此推荐让 run() 只作为稳定入口,把实际业务放在子进程中首次加载的 Service 中:

public function run(): mixed
{
    return (new \App\Service\CleanupExpiredTokenService())->run();
}

这样部署新的 Service 代码后,可以执行:

php easyswoole.php crontab reload --mode=produce

如果修改的是 Job 类自身、新增或删除 Job、调整 jobName()、crontabRule() 或注册逻辑,仍然需要完整重启。完整参数、进程范围、状态重置和强制重载风险参见Crontab 管理命令。

避免重复和堆积

普通 Crontab 任务共用 Worker。一次任务的执行时间超过调度间隔时,可能出现任务堆积或并发执行。建议:

  • 使用 Redis 锁或数据库唯一记录保证同一业务周期只执行一次。
  • 为外部请求和数据库查询设置超时。
  • 大批量工作拆成小任务,再投递到异步任务系统。
  • 在多实例部署中确认任务是否会在每个实例各执行一次;只允许单实例执行的任务必须增加分布式锁。
  • onException() 中记录任务名、计划时间和异常堆栈,避免任务静默失败。

源码依据