mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 15:32:48 +08:00
271 lines
12 KiB
Markdown
271 lines
12 KiB
Markdown
---
|
||
name: "ulthon-timer"
|
||
description: "内置秒级定时器(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.php](../../../app/common/command/Timer.php) / [TimerBase.php](../../../extend/base/common/command/TimerBase.php)
|
||
- [TimerService.php](../../../app/common/service/TimerService.php) / [TimerServiceBase.php](../../../extend/base/common/service/TimerServiceBase.php)
|
||
- [TimerController.php](../../../app/common/controller/TimerController.php) / [TimerControllerBase.php](../../../extend/base/common/controller/TimerControllerBase.php)
|
||
- 运行配置:[timer.php](../../../config/timer.php)
|
||
|
||
## 新增定时任务(默认规则)
|
||
|
||
无特殊情况下,新增定时任务应当通过本机制实现:在 `timer/config.php` 注册任务 + 以 `site` 或 `call` 的方式实现目标逻辑。
|
||
|
||
### 1) 选择任务类型
|
||
|
||
- `site`:通过 HTTP 访问一个控制器地址(默认优先使用)。即使任务需要“长时间运行”,也尽量设计成 `site` 模式(分片/分批/可重入),以复用框架的页面/接口同体、鉴权、日志、事务、限流等能力。
|
||
- `call`:直接执行一个 PHP callable(不推荐;除非万不得已)。仅在确实不适合走 HTTP 上下文、且不希望暴露为控制器入口时使用。
|
||
|
||
长时间运行任务的推荐写法(仍用 `site`):
|
||
|
||
- 设计为"可重入"的短任务:每次 `do()` 只处理一小批数据,处理进度写入缓存/表,下次继续
|
||
- 结合 `concurrency` 做分片:按 `$this->concurrencyId` 划分数据范围,多个实例并行推进
|
||
- 结合 `frequency` 控制节奏:用调度频率限制整体吞吐,避免单次占用过久
|
||
|
||
#### 按时间窗口循环处理(队列消费推荐)
|
||
|
||
适用于:需要持续消费队列/轮询数据的场景(如消息处理、订单状态同步、通知推送等)。
|
||
|
||
核心思路:在 `do()` 方法内设置一个**最大执行时间窗口**,循环处理单条数据,超时后自动退出,由定时器下次调度继续。
|
||
|
||
```php
|
||
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](../../../app/tools/controller/timer/ClearLog.php)、[ClearLogBase.php](../../../extend/base/tools/controller/timer/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` 或 `call`
|
||
- `target`:
|
||
- `site`:以 `/` 开头的相对路径(建议指向 `tools/timer.*` 控制器的 `do` 方法)
|
||
- `call`:callable
|
||
- `frequency`:执行频率(秒),小于 0 会被修正为 0
|
||
- `concurrency`:并发数量(默认 1)。`site` 类型会自动把并发参数写入 query
|
||
- `run_type`:调度策略(仅对 `site` 类型生效,`call` 类型不受影响)。可选值见「多节点协调 > run_type 调度」
|
||
|
||
示例(site):
|
||
|
||
```php
|
||
return [
|
||
[
|
||
'name' => 'clear_log',
|
||
'type' => 'site',
|
||
'target' => '/tools/timer.ClearLog/do',
|
||
'frequency' => 600,
|
||
'concurrency' => 1,
|
||
],
|
||
];
|
||
```
|
||
|
||
示例(call):
|
||
|
||
```php
|
||
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](../../../config/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 节流为准
|
||
- 本地开发忘记 `--local`:`site` 任务默认请求 `sysconfig('site','site_domain')` 指向的生产域名,不带 `--local` 会直接打到线上
|
||
|
||
## 多节点协调
|
||
|
||
定时器支持多节点部署,以数据库(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::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` | 节点状态、主节点管理 |
|
||
|
||
### 日志清理
|
||
|
||
```bash
|
||
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` 字段,标识主节点
|