- 删除 runParallel() 方法和所有 Workerman 引用(死代码) - 重写 runLoop() 为 Guzzle CurlMultiHandler 非阻塞事件循环 - 新增 pending 数组追踪进行中的请求,handler.tick() 非阻塞推进 - 自适应 sleep 策略(有任务 50ms,空闲 200ms) - 简化 config/timer.php:移除 mode,适配 Guzzle 参数 - 更新 SKILL.md:移除 parallel 描述,修正 --quit 文档 bug - 验证发现:--quiet 是 ThinkPHP 全局选项,不需要在 configure() 注册 - 验证发现:方法名不能用 run(),与 ThinkPHP Command::run() 签名冲突
12 KiB
name, description
| name | description |
|---|---|
| ulthon-timer | 内置秒级定时器(php think timer)的使用与扩展规范;用于新增/调整定时任务(site/call、并发分片、TimerController 防刷)。 |
timer(内置秒级定时器)
核心机制(你要记住的 3 件事)
- 任务配置统一从
app_file_path('common/command/timer/config.php')读取 - 每个配置会按
concurrency自动展开成多份任务实例,并自动注入concurrency_id/concurrency_count - “是否该执行”主要由定时器侧的 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 注册任务 + 以 site 或 call 的方式实现目标逻辑。
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.php、ClearLogBase.php
并发与防刷建议:
- 如需并发分片处理,在控制器中设置:
protected $concurrency = N;- 使用
$this->concurrencyId做分片编号(0 ~ N-1)
- 如希望防止外部重复请求(不仅是定时器自身节流),在控制器中设置:
protected $frequency = 秒数;- 这会启用
TimerControllerBase::protectVisit()基于 URL 的防刷限制
B. call 类型(函数任务)
目标形态:
target为call_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),不可重复type:site或calltarget:site:以/开头的相对路径(建议指向tools/timer.*控制器的do方法)call:callable
frequency:执行频率(秒),小于 0 会被修正为 0concurrency:并发数量(默认 1)。site类型会自动把并发参数写入 queryrun_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 表的保留天数,支持从 .env 的 TIMER_CLEAR_LOG_DAYS 覆盖 |
常见坑位(快速自检)
name重复:会导致 Cache key 冲突,表现为任务“莫名其妙不跑/跑得不对”concurrency与控制器侧$concurrency不一致:会触发concurrency id/count error- 只依赖控制器侧
$frequency:它只是防刷,不是调度;调度频率以定时器侧 Cache 节流为准
多节点协调
定时器支持多节点部署,以数据库(MySQL)作为协调中心。多个节点连接同一个数据库即可自动组成集群。
节点注册
每个节点启动后通过 system_host_register call 任务自动注册心跳(每 30 秒一次)。节点 ID 生成规则为 {hostname}-{8位md5},持久化在 runtime/node_id.lock 文件中,重启后保持不变。
主节点选举
- 第一个注册的节点自动成为主节点
- 管理员可在主机列表页面手动切换主节点
- 相关 API:
HostService::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_type、status),以管理后台的设置为准。
执行日志
TimerControllerBase 提供 logStart() / logEnd() 方法记录执行日志。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字段,标识主节点