Files
EasyGoAdmin_GoFrame_EleVue/document/wiki/09_定时任务调度.txt
T
2026-10-20 11:37:25 +08:00

184 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================================================================================
EasyGoAdmin 后端开发者文档 · 09 定时任务调度
================================================================================
代码位置:internal/logic/daemon.go(调度器)、internal/logic/job_handler.go
(本地处理器注册表)、internal/logic/job.go(/api/job 业务)、
internal/logic/job_log.go(任务日志)、internal/task/(任务实现)
表:goframe_job(任务定义)、goframe_job_log(执行日志)
--------------------------------------------------------------------------------
1. 架构总览
--------------------------------------------------------------------------------
数据驱动的常驻调度器:
启动(cmd.go 延时 2s)→ DaemonInit 加载 goframe_job 中 status=1 的任务
→ 按 cron_expression 挂载 gcron → tick 触发 executeJob
→ 执行目标分派(HTTP 调用 或 本地处理器)→ 结果写 goframe_job_log。
职责分层:
调度基础设施(挂载/摘除/锁/执行/落日志)在 internal/logic 的 job 模块;
任务业务实现在 internal/task 包(依赖方向单向:task → logic,logic 禁止
反向 import task);由 internal/cmd 空白导入 internal/task 触发 init 注册。
--------------------------------------------------------------------------------
2. 任务执行目标双形态(executeJob)
--------------------------------------------------------------------------------
读取任务行的 url 字段分派:
http:// 或 https:// 开头 → HTTP 调用目标地址:
params 为空 GET,非空 POST(Content-Type application/json,超时 30s);
非 200 或网络错误 → 失败(exception 记录 HTTP 状态与响应前 1000 字符)。
其余取值 → 视为本地处理方法名,从 jobHandlers 注册表查找:
签名 func(ctx context.Context, params string) (string, error)
params 为任务参数(JSON 字符串,可为空);返回摘要写入任务日志 message。
未注册 → 失败“未找到本地任务处理方法”。
执行防护:
* recover() 捕获 panic(不外泄影响调度器)
* force=false 时校验 status=1,否则从调度器摘除(数据被外部改动的自愈)
* 任务行不存在 → 摘除
--------------------------------------------------------------------------------
3. 执行策略与执行方式
--------------------------------------------------------------------------------
execute_policy(列值):
1 立即执行 允许并发:gcron.Add,tick 即启动新执行;不加执行锁
2 执行一次 单例调度(gcron.AddSingleton),上次未结束跳过本次;
叠加执行锁(默认策略)
3 放弃执行 并发 tick 照常触发,但抢不到执行锁则放弃本次
is_sync(列值):
1 同步 在调度协程内执行(阻塞本条目下一 tick 的派发)
0 异步 go 协程执行(context.Background())
执行锁 jobTryLock(策略 2/3 叠加):
键 job:lock:{id},utility/cache SET NX(TTL 300s 兜底——实例崩溃未释放时
自动过期;正常路径 defer 立即释放)。
多实例部署共享同一把锁(Redis 模式下防跨实例并发;内存降级时退化单机语义)。
cron 表达式:gf gcron 格式,默认 6 段(秒 分 时 日 月 周);
可用 # 前缀表示标准 5 段忽略秒(如 "# */5 * * * *" = 每 5 分钟)。
内置清理任务使用 6 段:"0 30 3 * * *" = 每日 03:30。
--------------------------------------------------------------------------------
4. 本地处理器注册表(job_handler.go + internal/task)
--------------------------------------------------------------------------------
logic.RegisterJobHandler(name, h):注册名全局唯一,重复注册进程直接 panic
(暴露配置错误)。注册名与任务记录 url 字段值一一对应(数据驱动查找)。
internal/task 包约定:
每任务一个文件(snake_case),init 中注册;
处理方法签名 func(ctx, params) (string, error),返回值写任务日志摘要;
不在 HTTP 启动链路内,由 internal/cmd 空白导入触发加载。
内置任务清单:
HelloWorld(hello_world.go) 示例任务,仅打日志,用于验证调度链路
SendSms(send_sms.go) 仅记录执行参数日志,不对接短信通道
LogClean(log_clean.go) 日志清理(见第 6 节)
--------------------------------------------------------------------------------
5. /api/job 管理接口(logic/job.go)
--------------------------------------------------------------------------------
七件套(page/list/detail/add/update/delete/batchDelete,perm sys:job:*)
+ 控制接口:
GET /api/job/runOnce/:id sys:job:execute 立即执行一次(异步
go executeJob(force=true),未发布任务也可执行)
GET /api/job/pause/:id sys:job:pause 摘除调度 + status=2(暂停)
GET /api/job/resume/:id sys:job:resume 重新挂载 + status=1(运行中)
POST /api/job/status sys:job:update 设状态(1→挂载,其他→摘除)
GET /api/job/daemonStatus 登录即可 调度器状态
(running=gcron 条目数、jobIds、startTime)
业务钩子(ModuleSpec 声明):
BeforeAdd 新增固定 status=0(未发布;发布走 Status/Resume)
AfterUpdate 运行中任务先摘除再按新配置重挂;挂载失败降级 status=2 并报
“Cron表达式无效”
BeforeDelete 存在性检查 + 摘除调度
status 语义:0 未发布 / 1 运行中 / 2 暂停。
枚举字典:job_status、job_execute_policy、job_is_sync(SerializeMaps 驱动
{field}Text 显示)。
5.1 调度执行流程图
启动(cmd, 延时2s) ─▶ DaemonInit(sync.Once)
│ SELECT * FROM goframe_job WHERE status=1
▼
逐条 addJobToScheduler(id, cron, policy, isSync)
│ policy=1 → gcron.Add (可并发,无锁)
│ policy≠1 → gcron.AddSingleton (单例)+ 执行锁
▼
tick 触发 ─▶ is_sync=1? ──是──▶ 同步执行 run()
│否
▼ go run(context.Background())
run(): policy≠1 → jobTryLock SET NX job:lock:{id}
│ 抢锁失败 → 放弃本次(Debug 日志)
▼ 抢到(defer 释放)
executeJob(id, force=false)
├─ 任务不存在/status≠1 → 摘除调度,返回
├─ url=http(s) → gclient GET/POST(30s 超时,params 为 body)
├─ url=方法名 → jobHandlers 查找执行(task 包注册)
└─ recover panic → 写日志
dao.JobLog.Insert(status/message/耗时/异常,create_user=daemon)
5.2 任务状态机
Add(固定置0)
│
▼
┌─────────┐ Status(1) / Resume ┌─────────┐
│ 0 未发布 │─────────────────────▶│ 1 运行中 │◀──┐
└─────────┘ └────┬────┘ │
▲ Status(2)/Pause │ │ Resume
│ ─────────────────────▶│ │
│ 摘除调度 ▼ │
│ ┌─────────┐ │
│ Status(0) │ 2 暂停 │───┘
│◀─────────────────────────── └─────────┘
│ 摘除调度
Delete:软删(del_flag=1) + 摘除调度
运行中 Update:摘除 → 按新 cron 重挂(失败降级置 2 并报 Cron 无效)
RunOnce:不看状态,异步 force 执行一次(不改状态)
--------------------------------------------------------------------------------
6. 内置任务 LogClean(task/log_clean.go)
--------------------------------------------------------------------------------
注册名 "LogClean";每日 02:30 由任务表配置驱动(内置任务数据在初始化菜单/
字典数据中配置;url 字段值 = LogClean)。
参数:{"retentionDays": n} 可覆盖配置 job.logRetentionDays(默认 90),
参数为 0/缺失取配置值,配置也无效则回退 90。
行为:分批物理删除三张日志表(goframe_operation_log / login_log / job_log)
中 create_time 早于 now-retentionDays 的记录:
单批 5000 行(logCleanBatch),循环至不足一批;
单表单次任务上限 200000 行(logCleanMaxTotal,防长事务与 runaway)。
摘要返回各表清理条数,写入任务日志 message。
--------------------------------------------------------------------------------
7. 与进程内 gcron 的其他条目
--------------------------------------------------------------------------------
调度器与内置清理共用 gcron,但命名空间隔离:
业务任务条目键 job:{id}(server/info 的 Scheduler.mounted 只统计 "job:" 前缀)
上传清理条目 upload_temp_clean(cmd.go 注册,每日 03:30 logic.CleanTemp,
天数取 file.tempExpireDays)
DaemonStatus 的 running 为 gcron 全部条目数(含非任务条目),
Scheduler.mounted 按 "job:" 前缀过滤——两处口径不同,排障时注意。
--------------------------------------------------------------------------------
8. 开发一个新定时任务的步骤
--------------------------------------------------------------------------------
1) internal/task/{任务名}.go 实现 handler:func(ctx, params) (string, error)
2) init() 中 logic.RegisterJobHandler("TaskName", handler)
(注册名=任务表 url 字段值;http(s) 地址则无需本地处理器)
3) 任务管理页新增记录:url 填 "TaskName",params 填 JSON 参数,
cron_expression 填 6 段或 # 5 段表达式,execute_policy/is_sync 按需
4) Status 置为运行中(发布)→ 调度器挂载;观察 job/log 执行日志
5) go build ./... 并重启(init 注册发生在进程加载期)
注意:handler 内可复用 base 共享能力(WithTrans/ChunkQuery 等)与 dao;
长任务用分批 + 上限;HTTP 目标任务自带 30s 超时,长任务应改为本地处理器。
--------------------------------------------------------------------------------
9. 多实例部署语义
--------------------------------------------------------------------------------
单实例:gcron 各自挂载,正常运行。
多实例 + Redis:策略 2/3 经 job:lock:{id} 全局互斥(同一时刻至多一个实例
在执行该任务);仍会重复“触发”(各实例各自 tick),但未抢到锁的实例直接放弃。
多实例 + 内存降级:锁退化为单机语义,可能出现跨实例并发执行——生产环境
Redis 必须可用。