Skip to content

6.11 定时任务 ​

概述

定时任务是数据驱动的常驻调度器:任务定义在 goframe_job 表,启动时加载 status=1 任务挂载 gcron,执行目标分双形态(HTTP 调用 / 本地处理器),三种执行策略叠加多实例执行锁,结果落 goframe_job_log。

架构总览 ​

text
启动(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 注册

执行目标双形态(executeJob 读 url 字段分派) ​

text
url 为 http:// 或 https:// 开头 → HTTP 调用目标地址:
  params 为空 GET,非空 POST(application/json,超时 30s)
  非 200 或网络错误 → 失败(exception 记录状态与响应前 1000 字符)

url 其余取值 → 本地处理器方法名,从 jobHandlers 注册表查找:
  签名 func(ctx context.Context, params string) (string, error)
  返回摘要写入任务日志 message
  未注册 → 失败"未找到本地任务处理方法"

执行策略与执行方式 ​

text
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 分钟)

调度流程与状态机 ​

text
                 Add(固定置0)
                   │
                   ▼
              ┌─────────┐  Status(1) / Resume   ┌─────────┐
              │ 0 未发布 │─────────────────────▶│ 1 运行中 │◀──┐
              └─────────┘                      └────┬────┘   │
                   ▲            Status(2)/Pause       │        │ Resume
                   │            ─────────────────────▶│        │
                   │            摘除调度               ▼        │
                   │                             ┌─────────┐   │
                   │            Status(0)        │ 2 暂停  │───┘
                   │◀─────────────────────────── └─────────┘
                   │  摘除调度
              Delete:软删 + 摘除调度
              运行中 Update:摘除 → 按新 cron 重挂(失败降级置 2 并报 Cron 无效)
              RunOnce:不看状态,异步 force 执行一次(不改状态)

管理接口(/api/job) ​

text
七件套 sys:job:*
  GET  /api/job/runOnce/:id        sys:job:execute   立即执行(异步,未发布也可执行)
  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/jobIds/startTime)

业务钩子:BeforeAdd 固定 status=0;AfterUpdate 运行中先摘除再重挂;
         BeforeDelete 存在性检查 + 摘除调度
字典:job_status / job_execute_policy / job_is_sync(SerializeMaps 驱动 {field}Text)

本地处理器注册(internal/task) ​

text
logic.RegisterJobHandler(name, h):注册名全局唯一,重复注册进程 panic
注册名与任务记录 url 字段值一一对应(数据驱动查找)

internal/task 包约定:
  每任务一个文件(snake_case),init 中注册
  处理方法签名 func(ctx, params) (string, error),返回值写任务日志摘要
  由 internal/cmd 空白导入触发加载(不在 HTTP 启动链路内)

内置任务:
  HelloWorld(hello_world.go)  示例,仅打日志(验证调度链路)
  SendSms(send_sms.go)       仅记录执行参数日志(不对接短信通道)
  LogClean(log_clean.go)     日志清理:每日 02:30 分批物理删除三张日志表
    参数 {"retentionDays": n} 可覆盖 job.logRetentionDays(默认 90)
    单批 5000 行循环至不足一批;单表单次任务上限 200000 行(防长事务/runaway)

新增任务开发步骤 ​

text
1) internal/task/{任务名}.go 实现 handler:func(ctx, params) (string, error)
2) init() 中 logic.RegisterJobHandler("TaskName", handler)
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 超时,长任务应改本地处理器

多实例部署语义 ​

text
单实例:gcron 各自挂载,正常运行
多实例 + Redis:策略 2/3 经 job:lock:{id} 全局互斥(同一时刻至多一个实例执行)
  ——仍会重复"触发"(各实例各自 tick),未抢到锁的实例直接放弃
多实例 + 内存降级:锁退化单机语义,可能跨实例并发执行——生产 Redis 必须可用

排障口径差异:daemonStatus.running 是 gcron 全部条目数(含 CleanTemp 等非任务条目);server/info 的 Scheduler.mounted 只统计 "job:" 前缀条目。

小蚂蚁云团队 · 提供技术支持