FORMA

定时任务

@nestjs/schedule 基于 cron 表达式调度任务。

NestJS 的 @nestjs/schedule 模块,是一个基于装饰器和 cronkelektiv/node-cron,npm 包名为 cron,注意与另一个同名不同源的 node-cron 包区分)库的轻量级任务调度工具。

它通过声明式装饰器和动态调度 API,为 Cron、Interval 和 Timeout 三类任务提供了开箱即用的支持。

一、声明式任务:@Cron, @Interval, @Timeout

在使用前,需要在根模块(通常是 AppModule)中导入并初始化 ScheduleModule

typescript
import { Module } from "@nestjs/common";
import { ScheduleModule } from "@nestjs/schedule";

@Module({
  imports: [ScheduleModule.forRoot()],
  // ...
})
export class AppModule {}

初始化完成后,便可通过以下三种装饰器来定义任务:

1. @Cron:Cron 表达式任务

这是最常用、最强大的任务调度方式,通过标准的 Cron 表达式定义执行周期。在下面的例子中,任务每 10 秒触发一次:

typescript
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Cron("*/10 * * * * *") // Cron 表达式
  handleCron() {
    this.logger.debug("Cron 任务执行");
  }
}
  • Cron 表达式参考
    • * * * * * *:每一秒
    • 45 * * * * *:每分钟的第 45 秒
    • 0 10 * * * *:每小时的第 10 分钟
    • 0 */30 9-17 * * *:每天 9 点到 17 点之间,每 30 分钟

此外,还支持 Date 对象和内置枚举 CronExpression(如 CronExpression.EVERY_DAY_AT_10AM)。

2. @Interval:固定间隔任务

适用于不需要精确到秒的、固定周期执行的任务。下面的代码会每 10 秒执行一次。

typescript
import { Injectable, Logger } from "@nestjs/common";
import { Interval } from "@nestjs/schedule";

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Interval(10000) // 单位:毫秒,即 10 秒
  handleInterval() {
    this.logger.debug("固定间隔任务执行");
  }
}

3. @Timeout:一次性延迟任务

它只会在应用启动后的指定时间后运行一次。

typescript
import { Injectable, Logger } from "@nestjs/common";
import { Timeout } from "@nestjs/schedule";

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Timeout(5000) // 5 秒后执行一次
  handleTimeout() {
    this.logger.debug("一次性任务执行");
  }
}

🎮 二、动态调度:SchedulerRegistry

ScheduleModule 的核心在于它的 SchedulerRegistry,它不仅管理着由装饰器创建的任务,还能让你在程序运行时动态地创建、启动、停止或删除任务。

下面的例子演示了如何动态添加一个 Cron 任务

typescript
import { Injectable, OnModuleInit } from "@nestjs/common";
import { SchedulerRegistry } from "@nestjs/schedule";
import { CronJob } from "cron";

@Injectable()
export class DynamicTaskService {
  constructor(private schedulerRegistry: SchedulerRegistry) {}

  addDynamicCronJob(cronId: string, seconds: string) {
    // 1. 创建 CronJob 实例
    const job = new CronJob(`${seconds} * * * * *`, () => {
      console.log(`动态任务 ${cronId} 执行!`);
    });

    // 2. 添加到注册表
    this.schedulerRegistry.addCronJob(cronId, job);
    job.start();
    console.log(`动态任务 ${cronId} 已添加并启动。`);
  }

  getCronJob(cronId: string) {
    return this.schedulerRegistry.getCronJob(cronId);
  }

  deleteCronJob(cronId: string) {
    this.schedulerRegistry.deleteCronJob(cronId);
    console.log(`动态任务 ${cronId} 已删除。`);
  }
}

类似地,通过 addIntervaladdTimeout 等方法也能动态添加对应的任务。

🌐 三、分布式锁:防止多实例重复执行

@nestjs/schedule 模块本身在 PM2 等集群模式下不提供防止任务重复执行的机制。因此,当应用同时运行多个实例时,同一个定时任务会在所有实例上被触发。

为了解决这个问题,实践中通常采用分布式锁(Redis 分布式锁)的方案。

🛠️ 方案一:集成 @nestjs/bullbull

这是一种非常健壮的方案。它将任务逻辑拆分为两个部分:

  1. 任务调度器:使用 @Cron 装饰器,但内部只是将任务信息添加到队列中。
  2. 队列处理器:由 Bull 队列负责,而 Bull 基于 Redis,天然就具备分布式特性,能保证一个任务只被一个消费者进程处理。
typescript
// cron-job.service.ts (调度器)
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { InjectQueue } from "@nestjs/bull";
import { Queue } from "bull";

@Injectable()
export class CronJobService {
  constructor(@InjectQueue("my-queue") private myQueue: Queue) {}

  @Cron("*/5 * * * * *") // 定时触发
  async scheduleJob() {
    await this.myQueue.add("my-job", {
      /* 任务数据 */
    }); // 将任务加入队列
  }
}

// my-queue.processor.ts (处理器)
import { Processor, Process } from "@nestjs/bull";
import { Job } from "bull";

@Processor("my-queue")
export class MyQueueProcessor {
  @Process("my-job")
  async handleCronJob(job: Job) {
    console.log("开始处理队列中的任务...", job.id);
    // 在这里执行真正的业务逻辑
  }
}

⚡ 方案二:使用 @RedisLock() 装饰器

对于不需要用到任务队列的场景,可以使用 nestjs-simple-redis-lock 这个第三方模块。它能以简洁的装饰器形式,为方法添加分布式锁的能力(具体参数以其官方文档为准,不同第三方库的 API 差异较大,接入前请仔细核对版本说明)。

typescript
import { Injectable } from "@nestjs/common";
import { RedisLock, RedisLockService } from "nestjs-simple-redis-lock";

@Injectable()
export class MyService {
  constructor(private readonly lockService: RedisLockService) {}

  // 参数依次为:锁名称、过期时间(毫秒,默认 60000)、重试间隔(毫秒)、最大重试次数
  @RedisLock("critical-task", 10000)
  async criticalTask() {
    console.log("执行关键任务,此过程被分布式锁保护");
    // ... 任务逻辑
  }
}

总而言之,@nestjs/schedule 是一款功能全面、设计优雅的任务调度模块。对于绝大多数单实例的应用,用其提供的声明式 API 就足够了;而当面对多实例部署的挑战时,Bullnestjs-simple-redis-lock 等分布式方案也提供了成熟的应对思路。

参考文献

以下链接在编写时均可正常访问:

资料说明
NestJS 文档官方
Task scheduling本章主题
Request lifecycle执行顺序

Series

new

6 / 19