周期任务与 Job Schedulers

周期性清理、日报生成和定时同步不能靠 setInterval 稳定完成:进程重启后计时器会丢失,多实例又可能重复执行。BullMQ 的 Job Scheduler 将调度配置保存在 Redis 中,并按规则持续产生普通任务。

创建或更新调度器

当前 API 是 queue.upsertJobScheduler()upsert 表示:ID 不存在时创建,已存在时更新,适合应用重复部署而不制造重复配置。

import { Queue } from "bullmq";

interface CleanupJobData {
  scope: "temporary-files" | "expired-sessions";
}

const maintenanceQueue = new Queue<CleanupJobData>("maintenance", {
  connection: { host: "127.0.0.1", port: 6379 },
});

await maintenanceQueue.upsertJobScheduler(
  "cleanup-temporary-files",
  { every: 60_000 },
  {
    name: "cleanup",
    data: { scope: "temporary-files" },
    opts: {
      attempts: 3,
      backoff: { type: "exponential", delay: 1_000 },
      removeOnComplete: 100,
      removeOnFail: 500,
    },
  },
);

第三个参数是任务模板,定义每次生成任务的名称、数据和选项。再次以相同 scheduler ID 调用即可更新规则或模板。

every:固定间隔

await maintenanceQueue.upsertJobScheduler(
  "session-cleanup",
  {
    every: 5 * 60_000,
    limit: 100,
  },
  {
    name: "cleanup",
    data: { scope: "expired-sessions" },
  },
);

every 使用毫秒。固定间隔会对齐到时间边界。当前 BullMQ 对新创建的 scheduler 会立即产生第一次执行;immediately 从 5.19.0 起已弃用,因为当前默认行为等同于首次立即执行。更新已存在的 scheduler 不会额外立即触发一次。

pattern:Cron 表达式

BullMQ 的 cron 模式支持可选的“秒”字段:

await maintenanceQueue.upsertJobScheduler(
  "daily-cleanup",
  {
    pattern: "0 15 3 * * *",
    tz: "Asia/Shanghai",
  },
  {
    name: "cleanup",
    data: { scope: "temporary-files" },
  },
);

上例表示每天 03:15:00 执行。显式配置 tz,不要依赖服务器本地时区。需要 UTC 时使用 tz: "UTC"

常用通用选项还包括:

调度频率不等于保证执行频率

Job Scheduler 在上一份任务开始处理时产生下一份任务。如果没有 Worker、并发不足或队列非常繁忙,任务不会为了追赶时间而无限堆积,实际处理间隔可能大于配置间隔。

这意味着:

查询和删除调度器

const scheduler = await maintenanceQueue.getJobScheduler(
  "cleanup-temporary-files",
);

const firstTen = await maintenanceQueue.getJobSchedulers(0, 9, true);

const removed = await maintenanceQueue.removeJobScheduler(
  "cleanup-temporary-files",
);

console.log({ scheduler, firstTen, removed });

周期任务会使用特殊任务 ID,不能为生成的每个任务指定自定义 jobId。需要区分任务时使用 scheduler ID、任务 name 和业务数据。

从旧 Repeatable API 演进而来

旧教程常见以下写法:

// 旧 API,仅用于识别历史代码
await maintenanceQueue.add(
  "cleanup",
  { scope: "temporary-files" },
  { repeat: { every: 60_000 } },
);

官方说明:旧 Repeatable APIs 从 BullMQ 5.16.0 起弃用,并在 v6 移除,改用 Job Schedulers。旧的 repeatgetRepeatableJobs()removeRepeatable() 等示例只适合历史版本。新项目应使用:

升级现有项目时不要只改方法名,应按官方 v5 到 v6 迁移指南检查已保存的旧调度数据。

实用技巧与最佳实践

本章小结

Job Scheduler 是持久化的“任务工厂”,every 适合固定间隔,pattern 适合日历时间。使用稳定 ID 配合 upsertJobScheduler() 可以安全部署和更新;同时要理解调度频率受 Worker 容量影响,不能替代业务上的补漏与幂等设计。

官方文档