Files

12 KiB
Raw Permalink Blame History

name, description
name description
ulthon-timer 内置秒级定时器php think timer的使用与扩展规范用于新增/调整定时任务site/call、并发分片、TimerController 防刷)。

timer内置秒级定时器

核心机制(你要记住的 3 件事)

  1. 任务配置统一从 app_file_path('common/command/timer/config.php') 读取
  2. 每个配置会按 concurrency 自动展开成多份任务实例,并自动注入 concurrency_id / concurrency_count
  3. “是否该执行”主要由定时器侧的 Cache 节流控制(可选叠加控制器侧的防刷保护)

相关实现入口:

  • 系统入口(优先从 app 层理解):
    • 定时器命令入口:app/common/command/Timer.php(实现继承自 extend/base/common/command/TimerBase.php
    • 任务实例服务入口:app/common/service/TimerService.php(实现继承自 extend/base/common/service/TimerServiceBase.php
    • site 任务控制器基类入口:app/common/controller/TimerController.php(实现继承自 extend/base/common/controller/TimerControllerBase.php
  • 实现文件(需要深入机制时再看 Base

新增定时任务(默认规则)

无特殊情况下,新增定时任务应当通过本机制实现:在 timer/config.php 注册任务 + 以 sitecall 的方式实现目标逻辑。

1) 选择任务类型

  • site:通过 HTTP 访问一个控制器地址(默认优先使用)。即使任务需要“长时间运行”,也尽量设计成 site 模式(分片/分批/可重入),以复用框架的页面/接口同体、鉴权、日志、事务、限流等能力。
  • call:直接执行一个 PHP callable不推荐除非万不得已。仅在确实不适合走 HTTP 上下文、且不希望暴露为控制器入口时使用。

长时间运行任务的推荐写法(仍用 site

  • 设计为"可重入"的短任务:每次 do() 只处理一小批数据,处理进度写入缓存/表,下次继续
  • 结合 concurrency 做分片:按 $this->concurrencyId 划分数据范围,多个实例并行推进
  • 结合 frequency 控制节奏:用调度频率限制整体吞吐,避免单次占用过久

按时间窗口循环处理(队列消费推荐)

适用于:需要持续消费队列/轮询数据的场景(如消息处理、订单状态同步、通知推送等)。

核心思路:在 do() 方法内设置一个最大执行时间窗口,循环处理单条数据,超时后自动退出,由定时器下次调度继续。

class MyQueueTask extends TimerController
{
    // 每次执行的最大运行时间(秒),根据业务和定时器 frequency 合理设置
    // 建议不超过 frequency 的一半,留出调度间隔
    protected $maxRunTime = 5;

    // 防刷间隔(秒),与定时器侧 frequency 配合
    protected $frequency = 10;

    public function do()
    {
        $maxTime = time() + $this->maxRunTime;

        do {
            $this->doItem();

            // 超时检查:时间窗口用完则退出,下次调度继续
            if (time() >= $maxTime) {
                break;
            }
        } while (true);
    }

    protected function doItem()
    {
        // 取一条待处理的数据
        $item = SomeModel::where('status', 0)->order('id', 'asc')->find();

        if (empty($item)) {
            // 队列为空时短暂休眠,避免空转消耗 CPU
            sleep(1);
            return;
        }

        // ... 处理单条数据的业务逻辑 ...

        $item->status = 1;
        $item->save();
    }
}

要点:

配置项 建议
maxRunTime 不超过 frequency 的一半;例如 frequency=10 时设 3~5 秒
doItem() 中的 sleep() 队列空时必须休眠,防止 CPU 空转;有数据时不要 sleep
doItem() 的粒度 每次只处理一条/一批数据,保证可重入、可中断
frequency maxRunTime 配合,frequency >= maxRunTime * 2 为宜

与普通定时任务的区别:

  • 普通任务:do() 执行一次就返回,靠定时器周期性调度
  • 时间窗口模式:do() 在时间窗口内持续循环消费,处理完积压数据后自动退出
  • 优势:面对突发积压数据时,单次调度可处理多条,提高吞吐;超时安全退出,不会阻塞定时器

2) 编写任务目标target

A. site 类型HTTP 任务)

实现方式建议:

  • 框架使用者(做业务):仅在 app/tools/controller/timer/ 新增控制器 *.php 即可;不需要、也不应该在 extend/base/ 增加 *Base.php
  • 框架作者(维护内核):在 extend/base/tools/controller/timer/ 新增 *Base.php 作为默认实现,同时在 app/tools/controller/timer/ 增加同名入口类 *.php 继承 Base系统唯一调用入口
  • 控制器建议继承 app\common\controller\TimerController(以获得并发参数校验与可选的 $frequency 防刷);执行入口一般提供 do()

示例(已有实现可参考):ClearLog.phpClearLogBase.php

并发与防刷建议:

  • 如需并发分片处理,在控制器中设置:
    • protected $concurrency = N;
    • 使用 $this->concurrencyId 做分片编号0 ~ N-1
  • 如希望防止外部重复请求(不仅是定时器自身节流),在控制器中设置:
    • protected $frequency = 秒数;
    • 这会启用 TimerControllerBase::protectVisit() 基于 URL 的防刷限制

B. call 类型(函数任务)

目标形态:

  • targetcall_user_func 可执行的 callable例如[SomeService::class, 'method']、闭包等
  • 由于 Base/App 双层机制的入口要求,类引用应指向 app/ 下的入口类(由入口类继承 Base 实现)
  • 长时间/重任务不建议用 call:它缺少 HTTP 上下文与控制器层通用能力,排错与复用成本更高

3) 注册到任务配置timer/config.php

默认配置文件位置(支持分层覆盖):

  • 优先覆盖:app/common/command/timer/config.php(一旦存在,app_file_path(...) 将优先读取此文件)
  • 框架默认:extend/base/common/command/timer/config.php(当 app 未提供时回落到该文件)

字段说明(兜底默认值由 Base 层的 initConfigItem() 提供):

  • name:任务唯一名称(用于 Cache key不可重复
  • typesitecall
  • target
    • site:以 / 开头的相对路径(建议指向 tools/timer.* 控制器的 do 方法)
    • callcallable
  • frequency:执行频率(秒),小于 0 会被修正为 0
  • concurrency:并发数量(默认 1site 类型会自动把并发参数写入 query
  • run_type:调度策略(仅对 site 类型生效,call 类型不受影响)。可选值见「多节点协调 > run_type 调度」

示例site

return [
    [
        'name' => 'clear_log',
        'type' => 'site',
        'target' => '/tools/timer.ClearLog/do',
        'frequency' => 600,
        'concurrency' => 1,
    ],
];

示例call

return [
    [
        'name' => 'system_host_register',
        'type' => 'call',
        'target' => [\app\common\service\HostService::class, 'heartbeat'],
        'frequency' => 30,
    ],
];

运行与验证

运行命令

  • 常规运行:php think timer
  • 只跑一轮(便于验证):php think timer --temp
  • 无任务时不输出“no request”php think timer --quiet

本地调试(指定请求 Host

site 任务会按站点域名发起请求,默认从 sysconfig('site','site_domain') 读取。

  • 本地调试:php think timer --local --local-host=http://localhost --local-port=8000

运行模式

配置在 timer.php。定时器使用 Guzzle CurlMultiHandler 实现非阻塞异步事件循环:

  • site 类型任务通过 curl multi 并行发送 HTTP 请求,真正非阻塞
  • call 类型任务在主循环中同步执行
  • pending 数组追踪进行中的请求,handler->tick() 非阻塞推进
  • 自适应 sleep 策略50ms/200ms避免 CPU 空转

配置项说明

配置键 默认值 说明
connect_timeout 30 连接超时时间(秒)
timeout 86400 请求响应超时时间(秒)
max_handles 100 curl multi 最大并发句柄数
select_timeout 0.001 curl_multi_select 超时(秒)
clear_log_days 3 ClearLog 任务清理 debug_log 表的保留天数,支持从 .envTIMER_CLEAR_LOG_DAYS 覆盖

常见坑位(快速自检)

  • name 重复:会导致 Cache key 冲突,表现为任务"莫名其妙不跑/跑得不对"
  • concurrency 与控制器侧 $concurrency 不一致:会触发 concurrency id/count error
  • 只依赖控制器侧 $frequency:它只是防刷,不是调度;调度频率以定时器侧 Cache 节流为准
  • 本地开发忘记 --localsite 任务默认请求 sysconfig('site','site_domain') 指向的生产域名,不带 --local 会直接打到线上

多节点协调

定时器支持多节点部署以数据库MySQL作为协调中心。多个节点连接同一个数据库即可自动组成集群。

节点注册

每个节点启动后通过 system_host_register call 任务自动注册心跳(每 30 秒一次)。节点 ID 生成规则为 {hostname}-{8位md5},持久化在 runtime/node_id.lock 文件中,重启后保持不变。

主节点选举

  • 第一个注册的节点自动成为主节点
  • 管理员可在主机列表页面手动切换主节点
  • 相关 APIHostService::setMasterNode() / HostService::getMasterNode()

run_type 调度

run_type 仅对 site 类型任务生效,call 类型任务始终在所有节点执行。可选值:

run_type 行为 适用场景
auto(默认) 两阶段 DB 行锁竞争BEGIN / SELECT FOR UPDATE / UPDATE / COMMIT 抢占,抢占成功后释放锁再执行。每个频率窗口内只有一个节点执行 通用场景
main 仅主节点执行 需要集中处理的任务
all 所有节点各自独立执行 节点本地清理等
manual 仅当 DB 中 manual_trigger=1 时执行,执行后自动重置为 0。通过管理后台触发 运维手动触发

配置同步

TimerService::syncConfigToDatabase() 在定时器启动时运行,将 PHP 配置文件中的任务同步到 system_timer_config 表。同步时不会覆盖数据库中已管理的字段(run_typestatus),以管理后台的设置为准。

执行日志

TimerControllerBase::execute() 自动包裹 do() 并调用 logStart() / logEnd() 记录执行日志,开发者无需手动调用。配置中写 /do,运行时 TimerServiceBase 自动重写为 /execute 以触发日志包裹。host_id 会自动注入到 site 任务的 URL 参数中,用于标识执行节点。

管理后台

页面 路径 功能
定时器配置 /admin/system.timer_config/index 管理 run_type、status、手动触发
定时器日志 /admin/system.timer_log/index 只读执行日志查看
主机列表 /admin/system.host/index 节点状态、主节点管理

日志清理

php think admin:timer:log:clean --days=30

相关数据表

  • ul_system_timer_config任务协调配置run_type、status、manual_trigger 等)
  • ul_system_timer_log:执行日志记录
  • ul_system_host:新增 is_master 字段,标识主节点