Skip to content
Go
周期性任务

周期性任务

1 概述

Asynq 提供两种周期性任务管理方式:

方式 适用场景 复杂度
Scheduler 简单的静态定时任务,cron 表达式硬编码
PeriodicTaskManager 需要动态增删定时任务,配置可外部化

两者均通过 Redis 分布式锁保证同一时刻只有一个调度实例在工作,避免重复触发。

2 Scheduler(基础调度器)

2.1 创建与配置

scheduler := asynq.NewScheduler(
    asynq.RedisClientOpt{Addr: "localhost:6379"},
    &asynq.SchedulerOpts{
        Location: time.Local,          // 时区
        EnqueueError: func(task *asynq.Task, opts []asynq.Option, err error) {
            log.Printf("scheduler enqueue error: %v", err)
        },
    },
)

2.2 SchedulerOpts 字段

字段 类型 默认值 说明
Location *time.Location time.UTC Cron 表达式解析的时区
Logger Logger 默认 调度器日志
LogLevel LogLevel InfoLevel 日志级别
EnqueueError func(task *Task, opts []Option, err error) 入队失败回调
HeartbeatInterval time.Duration 0 调度器心跳间隔(v0.25.1+)
PreEnqueueFunc func(task *Task, opts []Option) 入队前回调,可修改任务属性
PostEnqueueFunc func(info *TaskInfo, err error) 入队后回调

2.3 注册定时任务

task, _ := tasks.NewEmailTask(0, "daily-report")
entryID, err := scheduler.Register("0 9 * * *", task, asynq.Queue("default"))

Cron 表达式格式

┌─────────── 分钟 (0-59)
│ ┌─────────── 小时 (0-23)
│ │ ┌─────────── 日 (1-31)
│ │ │ ┌─────────── 月 (1-12)
│ │ │ │ ┌─────────── 星期 (0-6, 0=周日)
│ │ │ │ │
* * * * *

2.4 Cron 表达式示例

表达式 含义
"0 9 * * *" 每天上午 9:00
"0 0 1 * *" 每月 1 号 0:00
"*/5 * * * *" 每 5 分钟
"0 9 * * 1-5" 工作日(周一至周五)上午 9:00
"0 0,12 * * *" 每天 0:00 和 12:00
"@every 30s" 每 30 秒
"@every 1h30m" 每 1 小时 30 分钟
"@every 24h" 每 24 小时

2.5 启动与关闭

// 方式 1:阻塞运行
if err := scheduler.Run(); err != nil {
    log.Fatal(err)
}

// 方式 2:非阻塞
if err := scheduler.Start(); err != nil {
    log.Fatal(err)
}
defer scheduler.Shutdown()

2.6 注销定时任务

// 注册时获取 entryID
entryID, err := scheduler.Register("* * * * *", task)

// 根据需要注销
if err := scheduler.Unregister(entryID); err != nil {
    log.Printf("unregister failed: %v", err)
}

2.7 完整示例

func main() {
    scheduler := asynq.NewScheduler(
        asynq.RedisClientOpt{Addr: "localhost:6379"},
        &asynq.SchedulerOpts{
            Location: time.Local,
            EnqueueError: func(task *asynq.Task, opts []asynq.Option, err error) {
                log.Printf("enqueue error: task=%s err=%v", task.Type(), err)
            },
            PostEnqueueFunc: func(info *asynq.TaskInfo, err error) {
                if err != nil {
                    log.Printf("post enqueue error: %v", err)
                } else {
                    log.Printf("scheduled task enqueued: id=%s", info.ID)
                }
            },
        },
    )

    // 每日报表
    reportTask, _ := tasks.NewReportExportTask("daily", "pdf")
    scheduler.Register("0 9 * * *", reportTask, asynq.Queue("default"))

    // 每小时数据同步
    syncTask, _ := tasks.NewDataSyncTask()
    scheduler.Register("0 * * * *", syncTask, asynq.Queue("low"))

    // 每周清理(周日凌晨 3 点)
    cleanupTask, _ := tasks.NewCleanupTask()
    scheduler.Register("0 3 * * 0", cleanupTask, asynq.Queue("low"))

    if err := scheduler.Run(); err != nil {
        log.Fatal(err)
    }
}

3 PeriodicTaskManager(动态调度管理器)

PeriodicTaskManager 支持从外部配置源动态加载定时任务,适合需要运行时增减任务而不重启服务的场景。

3.1 组件架构

┌──────────────────────────────┐
│ PeriodicTaskConfigProvider   │  ← 实现此接口,从 DB/文件/配置中心读取配置
│ .GetConfigs()                │
└──────────┬───────────────────┘
           │ 定期同步 (SyncInterval)
           ▼
┌──────────────────────────────┐
│ PeriodicTaskManager          │
│ - 对比新旧配置                │
│ - 注册新任务                  │
│ - 注销已删除任务              │
│ - 通过 Redis 分布式锁防重复    │
└──────────────────────────────┘

3.2 基础用法

// 1. 定义配置提供者
type DBConfigProvider struct {
    db *sql.DB
}

func (p *DBConfigProvider) GetConfigs() ([]*asynq.PeriodicTaskConfig, error) {
    rows, err := p.db.Query("SELECT cron_expr, task_type, queue, payload FROM scheduled_tasks WHERE enabled = true")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var configs []*asynq.PeriodicTaskConfig
    for rows.Next() {
        var cronExpr, taskType, queue string
        var payload []byte
        rows.Scan(&cronExpr, &taskType, &queue, &payload)

        task := asynq.NewTask(taskType, payload)
        configs = append(configs, &asynq.PeriodicTaskConfig{
            Cronspec: cronExpr,
            Task:     task,
            Opts:     []asynq.Option{asynq.Queue(queue)},
        })
    }
    return configs, nil
}

// 2. 创建 PeriodicTaskManager
provider := &DBConfigProvider{db: db}
mgr, err := asynq.NewPeriodicTaskManager(
    asynq.PeriodicTaskManagerOpts{
        RedisConnOpt:               asynq.RedisClientOpt{Addr: "localhost:6379"},
        PeriodicTaskConfigProvider: provider,
        SyncInterval:               10 * time.Second, // 每 10 秒同步一次配置
    },
)
if err != nil {
    log.Fatal(err)
}

// 3. 启动
if err := mgr.Run(); err != nil {
    log.Fatal(err)
}

3.3 PeriodicTaskManagerOpts 字段

字段 类型 说明
RedisConnOpt RedisConnOpt Redis 连接配置
PeriodicTaskConfigProvider interface 提供定时任务配置的接口
SyncInterval time.Duration 同步配置的间隔(建议 5~60 秒)
SchedulerOpts *SchedulerOpts 底层 Scheduler 的配置
Logger Logger 自定义日志

3.4 PeriodicTaskConfig 结构

type PeriodicTaskConfig struct {
    Cronspec string       // Cron 表达式
    Task     *Task        // 要执行的 Task
    Opts     []Option     // 入队选项(Queue、MaxRetry 等)
}

3.5 分布式锁机制

PeriodicTaskManager 使用 Redis 分布式锁确保同一时刻只有一个实例在执行调度:

实例 A 获取锁 → 执行调度循环
实例 B 等待锁    → 锁被释放(A 崩溃/下线)
                → B 获取锁 → 继续调度

🔬 深入原理:锁通过 Redis 的 SET NX EX 实现,具有 TTL 过期保护。即使调度实例崩溃,锁也会在 TTL 后自动释放,由其他实例接管。

4 Scheduler vs PeriodicTaskManager 选择

场景 推荐方案
任务数量固定、cron 表达式不变 Scheduler
需要运行时动态增删任务 PeriodicTaskManager
配置通过配置文件管理 Scheduler(CI/CD 部署更新)
配置通过管理后台管理 PeriodicTaskManager
简单的几个定时任务 Scheduler
大规模多租户定时任务 PeriodicTaskManager + DB 存储

5 部署建议

┌──────────────────────┐
│   单独部署 Scheduler  │  ← 独立进程,仅运行 Scheduler/PeriodicTaskManager
│   (1 个实例即可)      │
└──────────────────────┘
            │
            │ Enqueue 定时任务
            ▼
┌──────────────────────┐
│   Worker 集群         │  ← 多个 Worker 进程处理任务
│   (可水平扩展)        │
└──────────────────────┘

💡 最佳实践

  • Scheduler 和 Worker 分开部署,避免调度瓶颈影响任务处理
  • Scheduler 部署 单实例即可(依赖 Redis 锁防止重复调度)
  • Worker 可按需水平扩展
  • 高可用场景:部署 2 个 Scheduler 实例,Redis 锁保证只有一个工作

6 常见问题

6.1 夏令时问题

// 使用固定时区避免夏令时导致的任务重复或遗漏
SchedulerOpts{Location: time.UTC}

// 或使用北京时间
loc, _ := time.LoadLocation("Asia/Shanghai")
SchedulerOpts{Location: loc}

6.2 任务耗时超过调度间隔

如果一个定时任务的执行时间超过了 cron 间隔,下一次调度不会重叠执行——Asynq 的调度是触发式的,仅在时间点将任务入队,不关心任务是否执行完成。

6.3 调度精度

Cron 的最小精度是分钟。如果需要更高精度(秒级),使用 @every 语法:

scheduler.Register("@every 10s", task)   // 每 10 秒
scheduler.Register("@every 500ms", task) // 不支持——最小 1 秒