如何设计一个简单易用的定时任务模块

如何设计一个简单易用的定时任务模块

项目地址

目录

  • 前言
  • 一、整体架构设计
    • [1.1 核心组件](#1.1 核心组件 "#11-%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6")
    • [1.2 核心接口定义](#1.2 核心接口定义 "#12-%E6%A0%B8%E5%BF%83%E6%8E%A5%E5%8F%A3%E5%AE%9A%E4%B9%89")
  • 二、任务模型设计
    • [2.1 任务结构](#2.1 任务结构 "#21-%E4%BB%BB%E5%8A%A1%E7%BB%93%E6%9E%84")
    • [2.2 执行策略](#2.2 执行策略 "#22-%E6%89%A7%E8%A1%8C%E7%AD%96%E7%95%A5")
    • [2.3 阻塞策略](#2.3 阻塞策略 "#23-%E9%98%BB%E5%A1%9E%E7%AD%96%E7%95%A5")
  • 三、执行器设计
    • [3.1 执行器接口](#3.1 执行器接口 "#31-%E6%89%A7%E8%A1%8C%E5%99%A8%E6%8E%A5%E5%8F%A3")
    • [3.2 自定义执行器示例](#3.2 自定义执行器示例 "#32-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%A7%E8%A1%8C%E5%99%A8%E7%A4%BA%E4%BE%8B")
    • [3.3 执行器注册](#3.3 执行器注册 "#33-%E6%89%A7%E8%A1%8C%E5%99%A8%E6%B3%A8%E5%86%8C")
  • 四、任务生命周期管理
    • [4.1 任务加载](#4.1 任务加载 "#41-%E4%BB%BB%E5%8A%A1%E5%8A%A0%E8%BD%BD")
    • [4.2 任务执行流程](#4.2 任务执行流程 "#42-%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B")
    • [4.3 结果处理](#4.3 结果处理 "#43-%E7%BB%93%E6%9E%9C%E5%A4%84%E7%90%86")
  • 五、API接口设计
    • [5.1 任务管理接口](#5.1 任务管理接口 "#51-%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86%E6%8E%A5%E5%8F%A3")
    • [5.2 执行结果接口](#5.2 执行结果接口 "#52-%E6%89%A7%E8%A1%8C%E7%BB%93%E6%9E%9C%E6%8E%A5%E5%8F%A3")
    • [5.3 创建任务示例](#5.3 创建任务示例 "#53-%E5%88%9B%E5%BB%BA%E4%BB%BB%E5%8A%A1%E7%A4%BA%E4%BE%8B")
  • 六、数据库设计
    • [6.1 任务表(sys_jobs)](#6.1 任务表(sys_jobs) "#61-%E4%BB%BB%E5%8A%A1%E8%A1%A8sys_jobs")
    • [6.2 执行结果表(sys_job_results)](#6.2 执行结果表(sys_job_results) "#62-%E6%89%A7%E8%A1%8C%E7%BB%93%E6%9E%9C%E8%A1%A8sys_job_results")
  • 七、高级特性
    • [7.1 超时控制](#7.1 超时控制 "#71-%E8%B6%85%E6%97%B6%E6%8E%A7%E5%88%B6")
    • [7.2 重试机制](#7.2 重试机制 "#72-%E9%87%8D%E8%AF%95%E6%9C%BA%E5%88%B6")
    • [7.3 并行控制](#7.3 并行控制 "#73-%E5%B9%B6%E8%A1%8C%E6%8E%A7%E5%88%B6")
  • 八、最佳实践
    • [8.1 执行器设计原则](#8.1 执行器设计原则 "#81-%E6%89%A7%E8%A1%8C%E5%99%A8%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99")
    • [8.2 任务配置建议](#8.2 任务配置建议 "#82-%E4%BB%BB%E5%8A%A1%E9%85%8D%E7%BD%AE%E5%BB%BA%E8%AE%AE")
    • [8.3 监控告警](#8.3 监控告警 "#83-%E7%9B%91%E6%8E%A7%E5%91%8A%E8%AD%A6")
  • 九、扩展性设计
    • [9.1 插件化支持](#9.1 插件化支持 "#91-%E6%8F%92%E4%BB%B6%E5%8C%96%E6%94%AF%E6%8C%81")
    • [9.2 动态注册](#9.2 动态注册 "#92-%E5%8A%A8%E6%80%81%E6%B3%A8%E5%86%8C")
  • 十、前端设计与使用
    • [10.1 前端页面结构](#10.1 前端页面结构 "#101-%E5%89%8D%E7%AB%AF%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84")
    • [10.2 任务管理页面功能](#10.2 任务管理页面功能 "#102-%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86%E9%A1%B5%E9%9D%A2%E5%8A%9F%E8%83%BD")
    • [10.3 任务创建/编辑表单](#10.3 任务创建/编辑表单 "#103-%E4%BB%BB%E5%8A%A1%E5%88%9B%E5%BB%BA%E7%BC%96%E8%BE%91%E8%A1%A8%E5%8D%95")
    • [10.4 执行日志页面](#10.4 执行日志页面 "#104-%E6%89%A7%E8%A1%8C%E6%97%A5%E5%BF%97%E9%A1%B5%E9%9D%A2")
    • [10.5 前端API调用示例](#10.5 前端API调用示例 "#105-%E5%89%8D%E7%AB%AFapi%E8%B0%83%E7%94%A8%E7%A4%BA%E4%BE%8B")
  • 十一、后端核心代码解读
    • [11.1 调度器核心实现](#11.1 调度器核心实现 "#111-%E8%B0%83%E5%BA%A6%E5%99%A8%E6%A0%B8%E5%BF%83%E5%AE%9E%E7%8E%B0")
    • [11.2 结果处理器实现](#11.2 结果处理器实现 "#112-%E7%BB%93%E6%9E%9C%E5%A4%84%E7%90%86%E5%99%A8%E5%AE%9E%E7%8E%B0")
    • [11.3 服务层设计](#11.3 服务层设计 "#113-%E6%9C%8D%E5%8A%A1%E5%B1%82%E8%AE%BE%E8%AE%A1")
    • [11.4 参数验证设计](#11.4 参数验证设计 "#114-%E5%8F%82%E6%95%B0%E9%AA%8C%E8%AF%81%E8%AE%BE%E8%AE%A1")
  • 十二、实际应用场景与案例
    • [12.1 典型应用场景](#12.1 典型应用场景 "#121-%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF")
    • [12.2 性能优化案例](#12.2 性能优化案例 "#122-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%A1%88%E4%BE%8B")
    • [12.3 故障处理案例](#12.3 故障处理案例 "#123-%E6%95%85%E9%9A%9C%E5%A4%84%E7%90%86%E6%A1%88%E4%BE%8B")
  • 十三、总结与展望
    • [13.1 设计总结](#13.1 设计总结 "#131-%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93")
    • [13.2 技术亮点](#13.2 技术亮点 "#132-%E6%8A%80%E6%9C%AF%E4%BA%AE%E7%82%B9")
    • [13.3 未来展望](#13.3 未来展望 "#133-%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B")
    • [13.4 适用场景](#13.4 适用场景 "#134-%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF")
  • 参考代码

前言

在日常开发中,我们经常遇到需要定时执行某些任务的场景,比如数据同步、报表生成、缓存清理、消息推送等。一个好的定时任务模块应该具备以下特点:

  • 简单易用:配置直观,上手快
  • 灵活可扩展:支持自定义任务类型
  • 可靠稳定:支持重试、超时控制
  • 可观测性:记录执行日志和结果

本文将基于一个实际项目中的定时任务模块设计,分享如何从零开始构建一个简单易用的定时任务系统。


一、整体架构设计

1.1 核心组件

我们的定时任务模块主要由以下几个核心组件构成:

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        API Layer                             │
│  (Controller: SysJobsController, SysJobResultsController)   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Service Layer                           │
│           (SysJobsService, SysJobResultsService)            │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Scheduler Layer                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │   Executor   │  │  Job Manager │  │Result Handler│     │
│  │   Registry   │  │              │  │              │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Storage Layer                           │
│              (sys_jobs, sys_job_results)                    │
└─────────────────────────────────────────────────────────────┘

1.2 核心接口定义

首先,我们定义了调度器的核心接口 JobSchedulerInterf

go 复制代码
type JobSchedulerInterf interface {
    // 生命周期管理
    Start()
    Stop()

    // 执行器管理
    RegisterExecutor(executor schedulerhelper.Executor)
    ListExecutors() []schedulerhelper.Executor

    // 任务管理
    AddOrUpdateJob(job *schedulerhelper.Job) (string, error)
    EnableJob(jobID string) error
    DisableJob(jobID string) error
    DeleteJob(jobID string) error
    ExecuteNow(jobID string) error
    ListJobs() []*schedulerhelper.Job
    JobExists(jobID string) bool

    // 结果获取
    GetResults() <-chan *schedulerhelper.JobResult
}

这个接口清晰地定义了调度器应该具备的所有能力,包括生命周期管理、执行器管理、任务管理和结果获取。


二、任务模型设计

2.1 任务结构

一个完整的任务定义包含以下关键信息:

go 复制代码
type Job struct {
    ID              string                 // 任务唯一标识
    Group           string                 // 任务分组
    Name            string                 // 任务名称
    Description     string                 // 任务描述
    ExecutorName    string                 // 执行器名称
    ExecutionPolicy ExecutionPolicy        // 执行策略:单次/重复
    Status          JobStatus              // 状态:启用/禁用
    CronExpression  string                 // Cron表达式
    Parameters      map[string]interface{} // 任务参数
    BlockingPolicy  BlockingPolicy         // 阻塞策略:丢弃/并行
    Timeout         time.Duration          // 超时时间
    MaxRetry        int                    // 最大重试次数
    RetryInterval   time.Duration          // 重试间隔
    ParallelNum     int                    // 并行数
    RunningCount    int                    // 当前运行数
}

2.2 执行策略

我们支持两种执行策略:

  • PolicyOnce(单次执行):任务执行成功后自动禁用
  • PolicyRepeat(重复执行):按照Cron表达式周期性执行

2.3 阻塞策略

当任务下一次执行时间到达,但上一次任务还在运行时,我们支持两种处理策略:

  • BlockDiscard(丢弃):跳过本次执行
  • BlockParallel(并行):允许并行执行(可配置最大并行数)

三、执行器设计

3.1 执行器接口

执行器是任务的实际执行单元,通过接口实现解耦:

go 复制代码
type Executor interface {
    Execute(ctx context.Context, job *Job) error
    Name() string
}

3.2 自定义执行器示例

实现一个自定义执行器非常简单,只需实现 Executor 接口:

go 复制代码
type DemoExecutor struct{}

func (e *DemoExecutor) Execute(ctx context.Context, job *schedulerhelper.Job) error {
    log.Printf("Executor %s executing job %s with params: %v",
        e.Name(), job.Name, job.Parameters)

    // 模拟任务执行
    select {
    case <-time.After(2 * time.Second):
        log.Printf("Job %s completed successfully", job.Name)
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (e *DemoExecutor) Name() string {
    return "demo-executor"
}

3.3 执行器注册

在应用启动时,通过 RegisterExecutors 函数注册所有执行器:

go 复制代码
func RegisterExecutors() {
    app.JobScheduler.RegisterExecutor(&executors.DemoExecutor{})
    // 添加更多执行器...
}

四、任务生命周期管理

4.1 任务加载

应用启动时,从数据库加载所有启用的任务:

go 复制代码
func LoadJobsFromDB() {
    ctx := context.Background()
    
    // 查询所有启用的任务
    var jobsList models.SysJobsList
    err := app.DB().WithContext(ctx).
        Model(&models.SysJobs{}).
        Where("status = ?", 1).
        Find(&jobsList).Error
        
    // 遍历任务列表,添加到调度器
    for _, job := range jobsList {
        // 解析参数、构建Job对象、添加到调度器
        app.JobScheduler.AddOrUpdateJob(schedulerJob)
    }
    
    // 启动结果处理器
    StartResultHandler()
}

4.2 任务执行流程

sql 复制代码
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ Cron Trigger│───▶│ Job Executor│───▶│Result Handler│
└─────────────┘    └─────────────┘    └─────────────┘
                           │                   │
                           ▼                   ▼
                    ┌─────────────┐    ┌─────────────┐
                    │   Executor  │    │   Database  │
                    │  Interface  │    │   Storage   │
                    └─────────────┘    └─────────────┘

4.3 结果处理

任务执行结果通过通道异步处理,避免阻塞调度器:

go 复制代码
func StartResultHandler() {
    go func() {
        resultsChan := app.JobScheduler.GetResults()
        
        for result := range resultsChan {
            // 保存结果到数据库
            saveJobResult(result)
            
            // 单次执行任务成功后自动禁用
            if result.ExecutionPolicy == schedulerhelper.PolicyOnce 
                && result.Status == "SUCCESS" {
                disableJob(result.JobID)
            }
        }
    }()
}

五、API接口设计

5.1 任务管理接口

接口 方法 说明
/api/sysJobs/list GET 获取任务列表(分页)
/api/sysJobs/:id GET 获取任务详情
/api/sysJobs/add POST 创建任务
/api/sysJobs/edit PUT 更新任务
/api/sysJobs/delete DELETE 删除任务
/api/sysJobs/setStatus PUT 设置任务状态
/api/sysJobs/executeNow POST 立即执行任务
/api/sysJobs/executors GET 获取执行器列表

5.2 执行结果接口

接口 方法 说明
/api/sysJobResults/list GET 获取执行结果列表(分页)
/api/sysJobResults/:id GET 获取执行结果详情
/api/sysJobResults/delete DELETE 删除执行结果

5.3 创建任务示例

json 复制代码
POST /api/sysJobs/add
{
  "group": "data-sync",
  "name": "用户数据同步",
  "description": "每天凌晨2点同步用户数据",
  "executorName": "user-sync-executor",
  "executionPolicy": 1,
  "status": 1,
  "cronExpression": "0 0 2 * * *",
  "parameters": "{\"batchSize\": 1000}",
  "blockingPolicy": 0,
  "timeout": 300000000000,
  "maxRetry": 3,
  "retryInterval": 60000000000,
  "parallelNum": 1
}

六、数据库设计

6.1 任务表(sys_jobs)

字段 类型 说明
id string 任务ID(主键)
group string 任务分组
name string 任务名称
description string 任务描述
executor_name string 执行器名称
execution_policy int 执行策略(0:单次 1:重复)
status int 状态(0:禁用 1:启用)
cron_expression string Cron表达式
parameters string 任务参数(JSON)
blocking_policy int 阻塞策略(0:丢弃 1:并行)
timeout bigint 超时时间(纳秒)
max_retry int 最大重试次数
retry_interval bigint 重试间隔(纳秒)
parallel_num int 并行数

6.2 执行结果表(sys_job_results)

字段 类型 说明
id string 结果ID(主键)
job_id string 任务ID
status string 执行状态
start_time timestamp 开始时间
end_time timestamp 结束时间
duration bigint 执行时长(纳秒)
retry_count int 重试次数
error text 错误信息
created_at timestamp 创建时间

七、高级特性

7.1 超时控制

每个任务可以设置超时时间,防止任务长时间阻塞:

go 复制代码
ctx, cancel := context.WithTimeout(context.Background(), job.Timeout)
defer cancel()

err := executor.Execute(ctx, job)

7.2 重试机制

任务执行失败时,支持自动重试:

go 复制代码
for i := 0; i <= job.MaxRetry; i++ {
    err := executor.Execute(ctx, job)
    if err == nil {
        break
    }
    
    if i < job.MaxRetry {
        time.Sleep(job.RetryInterval)
    }
}

7.3 并行控制

通过 RunningCountParallelNum 控制任务并行执行数量:

go 复制代码
if job.BlockingPolicy == BlockParallel {
    if job.RunningCount < job.ParallelNum {
        // 允许并行执行
        job.RunningCount++
    } else {
        // 达到并行上限,跳过本次执行
        return
    }
}

八、最佳实践

8.1 执行器设计原则

  1. 幂等性:执行器应该是幂等的,多次执行结果一致
  2. 超时处理:正确处理 context 的取消信号
  3. 错误处理:返回明确的错误信息
  4. 日志记录:记录关键步骤和异常信息

8.2 任务配置建议

  1. 合理设置超时:根据任务实际执行时间设置
  2. 适当重试:网络类任务可以重试,数据类任务谨慎重试
  3. 阻塞策略:短任务可并行,长任务建议丢弃
  4. 参数校验:在创建任务时校验参数有效性

8.3 监控告警

  1. 定期检查任务执行结果表
  2. 对失败任务设置告警
  3. 监控任务执行时长趋势

九、扩展性设计

9.1 插件化支持

系统支持通过插件方式扩展执行器,如示例插件:

go 复制代码
// plugins/example/scheduler/executors/example_executor.go
type ExampleExecutor struct{}

func (e *ExampleExecutor) Execute(ctx context.Context, job *schedulerhelper.Job) error {
    // 自定义执行逻辑
    return nil
}

func (e *ExampleExecutor) Name() string {
    return "example-executor"
}

9.2 动态注册

插件可以在加载时动态注册执行器:

go 复制代码
// plugins/example/scheduler/register.go
func RegisterExecutors() {
    app.JobScheduler.RegisterExecutor(&executors.ExampleExecutor{})
}

十、前端设计与使用

10.1 前端页面结构

根据系统菜单配置,定时任务模块包含两个主要页面:

菜单ID 路由 组件 标题 图标
140341 /sysjobs Sysjobs sysjobs functions
140342 /system/sysjobslist system/sysjobs/sysjobslist jobslist IconList
140347 /system/joblog system/sysjobresults/sysjobresultslist joblog IconHistory

11.2 任务管理页面功能

任务列表页面 (system/sysjobs/sysjobslist) 提供以下功能:

  1. 任务列表展示

    • 分页展示所有定时任务
    • 显示任务状态(启用/禁用)
    • 显示下次执行时间
    • 显示当前运行中的任务数
  2. 任务操作

    • 新增任务:点击"新增"按钮打开创建表单
    • 编辑任务:点击任务行进入编辑模式
    • 删除任务:支持单个删除
    • 启用/禁用:切换任务状态
    • 立即执行:手动触发任务执行
  3. 筛选功能

    • 按任务分组筛选
    • 按任务名称搜索
    • 按执行器名称筛选
    • 按状态筛选

11.3 任务创建/编辑表单

创建或编辑任务时,需要填写以下信息:

javascript 复制代码
// 任务表单数据结构
{
  group: "data-sync",           // 任务分组(必填)
  name: "用户数据同步",          // 任务名称(必填)
  description: "每天凌晨2点同步用户数据",
  executorName: "user-sync-executor",  // 执行器名称(必填,从下拉列表选择)
  executionPolicy: 1,           // 执行策略:0-单次执行,1-重复执行
  status: 1,                    // 状态:0-禁用,1-启用
  cronExpression: "0 0 2 * * *", // Cron表达式(必填)
  parameters: '{"batchSize": 1000}', // 任务参数(JSON格式)
  blockingPolicy: 0,            // 阻塞策略:0-丢弃,1-并行
  timeout: 300000000000,        // 超时时间(纳秒)
  maxRetry: 3,                  // 最大重试次数
  retryInterval: 60000000000,   // 重试间隔(纳秒)
  parallelNum: 1                // 并行数
}

11.4 执行日志页面

执行日志页面 (system/sysjobresults/sysjobresultslist) 提供以下功能:

  1. 日志列表展示

    • 分页展示任务执行记录
    • 显示执行状态(成功/失败)
    • 显示执行时长
    • 显示重试次数
    • 显示错误信息(如有)
  2. 日志操作

    • 查看详情:点击日志行查看完整执行信息
    • 删除日志:支持单个删除
  3. 筛选功能

    • 按任务ID筛选
    • 按执行状态筛选
    • 按时间范围筛选

11.5 前端API调用示例

javascript 复制代码
// 获取任务列表
async function getJobList(params) {
  const response = await axios.get('/api/sysJobs/list', { params });
  return response.data;
}

// 创建任务
async function createJob(data) {
  const response = await axios.post('/api/sysJobs/add', data);
  return response.data;
}

// 更新任务
async function updateJob(data) {
  const response = await axios.put('/api/sysJobs/edit', data);
  return response.data;
}

// 删除任务
async function deleteJob(id) {
  const response = await axios.delete('/api/sysJobs/delete', {
    params: { id }
  });
  return response.data;
}

// 设置任务状态
async function setJobStatus(id, status) {
  const response = await axios.put('/api/sysJobs/setStatus', null, {
    params: { id, status }
  });
  return response.data;
}

// 立即执行任务
async function executeJobNow(id) {
  const response = await axios.post('/api/sysJobs/executeNow', null, {
    params: { id }
  });
  return response.data;
}

// 获取执行器列表
async function getExecutors() {
  const response = await axios.get('/api/sysJobs/executors');
  return response.data;
}

// 获取执行结果列表
async function getJobResults(params) {
  const response = await axios.get('/api/sysJobResults/list', { params });
  return response.data;
}

十一、后端核心代码解读

11.1 调度器核心实现

调度器是整个定时任务模块的核心,基于 robfig/cron 库实现。

12.1.1 调度器结构
go 复制代码
type JobScheduler struct {
    mu         sync.RWMutex           // 读写锁,保护并发访问
    cron       *cron.Cron             // cron调度器
    jobs       map[string]*Job        // 任务存储
    executors  map[string]Executor    // 执行器存储
    jobResults chan *JobResult        // 任务结果通道
    logger     JobLogger              // 日志记录器
    wg         sync.WaitGroup         // 等待正在执行的任务完成
}

设计要点

  • 使用 sync.RWMutex 保护共享数据,支持高并发读写
  • 使用通道异步传递执行结果,避免阻塞调度器
  • 使用 sync.WaitGroup 确保优雅关闭时所有任务完成
12.1.2 任务添加流程
go 复制代码
func (s *JobScheduler) AddOrUpdateJob(job *Job) (string, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    // 1. 判断是新增还是更新
    action := "更新"
    if _, exists := s.jobs[job.ID]; !exists {
        action = "创建"
        if job.ID == "" {
            job.ID = generateJobID()  // 自动生成ID
        }
        job.CreatedAt = time.Now()
    }
    
    // 2. 验证任务配置
    if err := validateJob(job); err != nil {
        return "", err
    }
    
    // 3. 如果任务已存在且已调度,先移除
    if existingJob, exists := s.jobs[job.ID]; exists && existingJob.cronEntryID != 0 {
        s.cron.Remove(existingJob.cronEntryID)
    }
    
    // 4. 如果任务启用状态,添加到cron调度
    if job.Status == StatusEnabled {
        entryID, err := s.cron.AddFunc(job.CronExpression, s.createJobFunc(job))
        if err != nil {
            return "", fmt.Errorf("failed to add job to cron: %v", err)
        }
        job.cronEntryID = entryID
    }
    
    // 5. 保存到内存
    s.jobs[job.ID] = job
    s.logger.LogJobLifecycle(job, action)
    return job.ID, nil
}

关键设计

  • 支持任务ID自动生成
  • 更新任务时先移除旧的调度,再添加新的调度
  • 只有启用状态的任务才会被添加到cron调度器
12.1.3 任务执行流程
go 复制代码
func (s *JobScheduler) executeJob(job *Job) {
    s.wg.Add(1)
    defer s.wg.Done()
    
    startTime := time.Now()
    jobExecutionID := fmt.Sprintf("%s-%d", job.ID, startTime.UnixNano())
    
    // 重试循环
    for currentRetry := 0; currentRetry <= job.MaxRetry; currentRetry++ {
        result := &JobResult{
            JobID:           job.ID,
            StartTime:       time.Now(),
            RetryCount:      currentRetry,
            ExecutionPolicy: job.ExecutionPolicy,
        }
        
        // 1. 获取执行器
        s.mu.RLock()
        executor, exists := s.executors[job.ExecutorName]
        s.mu.RUnlock()
        
        if !exists {
            result.Status = "FAILED"
            result.Error = fmt.Errorf("executor not found: %s", job.ExecutorName)
            s.sendResult(result)
            return
        }
        
        // 2. 创建带超时的上下文
        ctx, cancel := context.WithTimeout(context.Background(), job.Timeout)
        
        // 3. 执行任务(传递深拷贝,避免并发修改)
        jobCopy := job.Clone()
        err = executor.Execute(ctx, jobCopy)
        cancel()
        
        result.EndTime = time.Now()
        result.Duration = result.EndTime.Sub(result.StartTime)
        
        // 4. 处理执行结果
        if err != nil {
            result.Status = "FAILED"
            result.Error = err
            
            if currentRetry < job.MaxRetry {
                time.Sleep(job.RetryInterval)
                continue  // 继续重试
            } else {
                s.sendResult(result)
                return  // 达到最大重试次数
            }
        } else {
            result.Status = "SUCCESS"
            s.sendResult(result)
            
            // 5. 单次执行策略:执行成功后自动禁用
            if job.ExecutionPolicy == PolicyOnce {
                s.DisableJob(job.ID)
            }
            return
        }
    }
}

关键设计

  • 使用 context.WithTimeout 实现超时控制
  • 使用 job.Clone() 创建深拷贝,避免并发修改问题
  • 支持失败重试,重试间隔可配置
  • 单次执行任务成功后自动禁用
12.1.4 阻塞策略实现
go 复制代码
func (s *JobScheduler) canExecute(job *Job) bool {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    currentJob, exists := s.jobs[job.ID]
    if !exists {
        return false
    }
    
    runningCount := currentJob.RunningCount
    
    switch job.BlockingPolicy {
    case BlockDiscard:
        // 丢弃策略:同一时间只能有一个任务执行
        if runningCount == 0 {
            return true
        }
        return false
        
    case BlockParallel:
        // 并行策略:允许指定数量的任务并行执行
        if job.ParallelNum <= 0 {
            return true  // 无限制
        }
        if runningCount < job.ParallelNum {
            return true
        }
        return false
        
    default:
        // 默认策略与 BlockDiscard 相同
        return runningCount == 0
    }
}

关键设计

  • BlockDiscard:确保任务串行执行,避免资源竞争
  • BlockParallel:支持并行执行,提高吞吐量
  • 通过 RunningCount 实时跟踪运行中的任务数

12.2 结果处理器实现

结果处理器是一个后台协程,负责将任务执行结果持久化到数据库。

go 复制代码
func StartResultHandler() {
    ctx, cancel := context.WithCancel(context.Background())
    resultHandlerCancel = cancel
    
    go func() {
        resultsChan := app.JobScheduler.GetResults()
        
        for {
            select {
            case <-ctx.Done():
                // 处理剩余的结果
                drainResults(resultsChan)
                return
                
            case result, ok := <-resultsChan:
                if !ok {
                    return  // 通道已关闭
                }
                
                // 保存结果到数据库
                if err := saveJobResult(result); err != nil {
                    app.ZapLog.Error("保存任务结果失败",
                        zap.String("jobID", result.JobID),
                        zap.Error(err))
                }
            }
        }
    }()
}

关键设计

  • 使用 context.Context 支持优雅关闭
  • 通道关闭时处理剩余结果,确保数据不丢失
  • 异步处理,不阻塞调度器

11.3 服务层设计

服务层负责业务逻辑处理,是控制器和调度器之间的桥梁。

11.3.1 创建任务流程
go 复制代码
func (s *SysJobsService) Create(c *gin.Context, req models.SysJobsCreateRequest) (*models.SysJobs, error) {
    // 1. 验证Cron表达式
    if err := schedulerhelper.ValidateCronExpression(req.CronExpression); err != nil {
        return nil, errors.New("Cron表达式格式错误: " + err.Error())
    }
    
    // 2. 解析任务参数JSON字符串
    var parameters map[string]interface{}
    if req.Parameters != "" {
        if err := json.Unmarshal([]byte(req.Parameters), &parameters); err != nil {
            return nil, errors.New("任务参数JSON格式错误: " + err.Error())
        }
    }
    
    // 3. 构建调度器Job对象
    job := &schedulerhelper.Job{
        Group:           req.Group,
        Name:            req.Name,
        Description:     req.Description,
        ExecutorName:    req.ExecutorName,
        ExecutionPolicy: schedulerhelper.ExecutionPolicy(req.ExecutionPolicy),
        Status:          schedulerhelper.JobStatus(req.Status),
        CronExpression:  req.CronExpression,
        Parameters:      parameters,
        BlockingPolicy:  schedulerhelper.BlockingPolicy(req.BlockingPolicy),
        Timeout:         time.Duration(req.Timeout),
        MaxRetry:        req.MaxRetry,
        RetryInterval:   time.Duration(req.RetryInterval),
        ParallelNum:     req.ParallelNum,
    }
    
    // 4. 调用调度器AddOrUpdateJob生成jobID
    jobID, err := app.JobScheduler.AddOrUpdateJob(job)
    if err != nil {
        return nil, errors.New("添加任务到调度器失败: " + err.Error())
    }
    
    // 5. 创建sys_jobs记录
    sysJobs := models.NewSysJobs()
    sysJobs.Id = jobID
    // ... 设置其他字段
    
    // 6. 保存到数据库
    if err := sysJobs.Create(c); err != nil {
        return nil, err
    }
    
    return sysJobs, nil
}

关键设计

  • 先验证参数,再调用调度器
  • 调度器和数据库双重存储,确保数据一致性
  • 错误信息清晰,便于调试

11.4 参数验证设计

使用结构体标签实现参数验证:

go 复制代码
type SysJobsCreateRequest struct {
    Validator
    Group           string `form:"group" validate:"required" message:"任务分组名称不能为空"`
    Name            string `form:"name" validate:"required" message:"任务名称不能为空"`
    ExecutorName    string `form:"executorName" validate:"required" message:"执行器名称不能为空"`
    CronExpression  string `form:"cronExpression" validate:"required" message:"Cron表达式不能为空"`
    // ... 其他字段
}

关键设计

  • 使用 validate 标签定义验证规则
  • 使用 message 标签自定义错误消息
  • 统一的验证接口,便于扩展

十二、实际应用场景与案例

12.1 典型应用场景

12.1.1 数据同步任务
  • 场景:每天凌晨2点同步用户数据到数据仓库
  • 配置
    • 执行器:user-sync-executor
    • Cron表达式:0 0 2 * * *
    • 参数:{"batchSize": 1000, "source": "mysql", "target": "data_warehouse"}
    • 超时时间:30分钟
    • 重试次数:3次
12.1.2 报表生成任务
  • 场景:每周一上午8点生成周报
  • 配置
    • 执行器:report-generator-executor
    • Cron表达式:0 0 8 * * 1
    • 参数:{"reportType": "weekly", "recipients": ["manager@company.com"]}
    • 执行策略:重复执行
12.1.3 缓存清理任务
  • 场景:每小时清理过期的缓存数据
  • 配置
    • 执行器:cache-cleaner-executor
    • Cron表达式:0 0 * * * *
    • 参数:{"cacheType": "redis", "ttl": 3600}
    • 阻塞策略:并行执行(允许多个实例同时运行)

12.2 性能优化案例

12.2.1 高并发场景优化
  • 问题:大量任务同时触发导致系统负载过高
  • 解决方案
    1. 使用 BlockParallel 策略限制并行数
    2. 调整任务的执行时间,错峰执行
    3. 增加任务超时时间,避免长时间阻塞
12.2.2 大数据量任务优化
  • 问题:数据同步任务处理大量数据时内存占用过高
  • 解决方案
    1. 使用分批次处理,设置合理的 batchSize
    2. 增加任务超时时间
    3. 监控任务执行时长,及时调整参数

12.3 故障处理案例

12.3.1 网络异常处理
  • 场景:外部API调用失败
  • 处理方案
    1. 配置合理的重试次数和重试间隔
    2. 在任务参数中记录失败原因
    3. 设置告警机制,及时通知管理员
12.3.2 数据库连接失败
  • 场景:数据库维护期间任务执行失败
  • 处理方案
    1. 使用指数退避算法进行重试
    2. 记录详细的错误日志
    3. 任务状态自动恢复机制

十三、总结与展望

13.1 设计总结

本文介绍了一个简单易用的定时任务模块设计,主要特点包括:

  1. 清晰的分层架构:API层、服务层、调度器层职责分明,便于维护和扩展
  2. 灵活的执行器机制:通过接口实现任务类型的扩展,支持插件化开发
  3. 完善的任务管理:支持任务的CRUD、状态控制、立即执行等完整生命周期管理
  4. 可靠的结果处理:异步保存执行结果,支持重试、超时控制和阻塞策略
  5. 良好的可观测性:完整的日志记录和结果追踪,便于问题排查
  6. 优秀的前后端集成:提供完整的API接口和前端页面,开箱即用

13.2 技术亮点

  1. 并发安全设计:使用读写锁保护共享数据,支持高并发场景
  2. 优雅关闭机制:支持平滑关闭,确保所有任务执行完成
  3. 配置化驱动:所有任务参数均可配置,无需修改代码
  4. 插件化架构:支持通过插件扩展执行器,便于功能扩展
  5. 完善的错误处理:支持重试、超时、错误记录等机制

13.3 未来展望

  1. 分布式调度:支持多节点部署,实现任务的高可用和负载均衡
  2. 可视化编排:提供图形化界面,支持复杂任务流程的编排
  3. 智能调度:基于历史执行数据,智能调整任务执行时间和资源分配
  4. 监控告警增强:集成更强大的监控告警系统,支持多种通知方式
  5. 性能优化:支持任务执行结果的压缩存储,减少存储空间占用

13.4 适用场景

本定时任务模块适用于以下场景:

  • 企业内部系统的定时任务管理
  • 中小型项目的定时任务需求
  • 需要灵活扩展执行器类型的场景
  • 对任务执行可靠性和可观测性有要求的项目

参考代码


作者注:本文基于实际项目代码整理,详细介绍了如何设计一个简单易用的定时任务模块。该设计已经在生产环境中稳定运行,支撑了多种类型的定时任务。希望本文的设计思路能够对大家有所帮助,如有疑问欢迎交流讨论。

相关推荐
Bigger1 天前
告别版本焦虑:如何为 Hugo 项目定制专属构建环境
前端·架构·go
刀法如飞2 天前
一款Go语言Gin框架MVC脚手架,满足大部分场景
go·mvc·gin
Coding君2 天前
每日一Go-26、Go语言进阶:深入并发模式2
go
怕浪猫2 天前
第19章:Go语言工具链与工程实践
后端·go·编程语言
tyung3 天前
Go 为什么没成为游戏服务器主流语言
go
F1FJJ3 天前
基于网络隐身的内网穿透
网络协议·网络安全·go
凉凉的知识库3 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go
Nyarlathotep01134 天前
Go语言http请求过程分析
go