首页 / NestJS 入门教程 / 定时任务

NestJS 入门教程

定时任务

本教程共 47 篇 · 第 31 篇 · 更新于 2026-08-09 · 约 13 分钟阅读

NestJS定时任务CronScheduleSchedulerRegistry任务调度

本节目标:掌握 NestJS 中的定时任务调度,包括 Cron 任务、Interval 任务、Timeout 任务和动态调度管理。

有些任务不需要用户触发,而是自动执行。比如每天凌晨清理过期数据、每小时生成报表、每 5 分钟检查一次系统状态。

Linux 系统里用 cron 干这事。Node.js 世界里,NestJS 提供了 @nestjs/schedule 模块,用起来更优雅。

安装和配置

npm install @nestjs/schedule

在根模块里启用调度:

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

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

这样定时任务的调度器就启动了。

Cron 任务

Cron 任务是最常用的定时任务类型。可以精确控制任务在什么时间执行。

基础用法

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

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

  @Cron('45 * * * * *')
  handleCron() {
    this.logger.debug('每分钟第 45 秒执行');
  }
}

这段代码的意思是:每分钟的第 45 秒执行一次。

Cron 表达式

Cron 表达式长这样:

* * * * * *
| | | | | |
| | | | | 星期 (0-6, 0是周日)
| | | | 月份 (1-12)
| | | 日期 (1-31)
| | 小时 (0-23)
| 分钟 (0-59)
秒 (0-59)

几个常见的例子:

表达式含义
* * * * * *每秒
0 * * * * *每分钟
0 0 * * * *每小时
0 0 0 * * *每天午夜
0 30 11 * * 1-5工作日 11:30
0 0 0 1 * *每月 1 号
Tip

秒是可选的。如果只有 5 个字段,就表示从分钟开始。

预定义表达式

手写 Cron 表达式容易出错。NestJS 提供了常用的预定义值:

import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Cron(CronExpression.EVERY_MINUTE)
  everyMinute() {
    // 每分钟执行
  }

  @Cron(CronExpression.EVERY_HOUR)
  everyHour() {
    // 每小时执行
  }

  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  everyDayAtMidnight() {
    // 每天午夜执行
  }

  @Cron(CronExpression.EVERY_WEEK)
  everyWeek() {
    // 每周执行
  }

  @Cron(CronExpression.EVERY_WEEKDAY)
  everyWeekday() {
    // 每个工作日执行
  }
}

指定时区

服务器时区和业务时区不一致?指定时区:

@Cron('0 0 9 * * 1-5', {
  name: 'weekdayTask',
  timeZone: 'Asia/Shanghai',
})
handleWeekdayTask() {
  // 工作日早上 9 点执行(上海时间)
}

一次性任务

想在某个特定时间执行一次?传一个 Date 对象:

@Cron(new Date('2026-12-31T23:59:00'))
handleNewYearEve() {
  // 2026 年最后一秒执行
}

或者相对当前时间:

@Cron(new Date(Date.now() + 10000))
handleAfter10Seconds() {
  // 应用启动 10 秒后执行一次
}

Interval 任务

固定间隔执行,比如每 30 秒检查一次:

import { Injectable } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Interval(10000)
  handleInterval() {
    // 每 10 秒执行一次
  }

  @Interval('taskName', 5000)
  handleNamedInterval() {
    // 每 5 秒执行一次,带名称
  }
}
Note

底层用的是 JavaScript 的 setInterval()

Timeout 任务

应用启动后延迟执行一次:

import { Injectable } from '@nestjs/common';
import { Timeout } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Timeout(5000)
  handleTimeout() {
    // 应用启动后 5 秒执行一次
  }

  @Timeout('onceTask', 10000)
  handleNamedTimeout() {
    // 应用启动后 10 秒执行一次,带名称
  }
}

适合做初始化任务,比如预热缓存、加载配置。

动态调度

装饰器声明的任务是固定的。如果想运行时动态添加、删除、暂停任务,得用 SchedulerRegistry

注入 SchedulerRegistry

import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';

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

动态添加 Cron 任务

addCronJob(name: string, cronTime: string, callback: () => void) {
  const job = new CronJob(cronTime, callback);
  this.schedulerRegistry.addCronJob(name, job);
  job.start();
}

控制任务

// 暂停任务
stopCronJob(name: string) {
  const job = this.schedulerRegistry.getCronJob(name);
  job.stop();
}

// 启动任务
startCronJob(name: string) {
  const job = this.schedulerRegistry.getCronJob(name);
  job.start();
}

// 删除任务
deleteCronJob(name: string) {
  this.schedulerRegistry.deleteCronJob(name);
}

// 查看所有任务
getCronJobs() {
  const jobs = this.schedulerRegistry.getCronJobs();
  jobs.forEach((job, name) => {
    console.log(`任务: ${name}, 下次执行: ${job.nextDate()}`);
  });
}

动态 Interval 和 Timeout

// 添加 Interval
addInterval(name: string, milliseconds: number, callback: () => void) {
  const interval = setInterval(callback, milliseconds);
  this.schedulerRegistry.addInterval(name, interval);
}

// 删除 Interval
clearInterval(name: string) {
  this.schedulerRegistry.deleteInterval(name);
}

// 添加 Timeout
addTimeout(name: string, milliseconds: number, callback: () => void) {
  const timeout = setTimeout(callback, milliseconds);
  this.schedulerRegistry.addTimeout(name, timeout);
}

// 删除 Timeout
clearTimeout(name: string) {
  this.schedulerRegistry.deleteTimeout(name);
}

实际应用示例

数据清理

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThan } from 'typeorm';
import { Session } from './entities/session.entity';

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

  constructor(
    @InjectRepository(Session)
    private sessionRepository: Repository<Session>,
  ) {}

  @Cron(CronExpression.EVERY_DAY_AT_3AM)
  async cleanExpiredSessions() {
    this.logger.log('开始清理过期会话...');
    
    const result = await this.sessionRepository.delete({
      expiresAt: LessThan(new Date()),
    });

    this.logger.log(`清理了 ${result.affected} 条过期会话`);
  }
}

报表生成

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

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

  constructor(
    private reportsService: ReportsService,
    private emailService: EmailService,
  ) {}

  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  async generateDailyReport() {
    this.logger.log('生成日报...');
    const report = await this.reportsService.generateDailyReport();
    await this.emailService.sendReport('admin@example.com', report);
  }

  @Cron('0 0 0 * * 0') // 每周日
  async generateWeeklyReport() {
    this.logger.log('生成周报...');
    const report = await this.reportsService.generateWeeklyReport();
    await this.emailService.sendReport('admin@example.com', report);
  }
}

缓存预热

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

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

  constructor(
    private cacheService: CacheService,
    private productsService: ProductsService,
  ) {}

  @Cron(CronExpression.EVERY_HOUR)
  async warmupProductCache() {
    this.logger.log('预热商品缓存...');
    const products = await this.productsService.findAll();
    await this.cacheService.set('products:all', products, 3600);
    this.logger.log(`缓存了 ${products.length} 个商品`);
  }
}

最佳实践

错误处理

定时任务里的错误要捕获,不然会静默失败:

@Cron(CronExpression.EVERY_HOUR)
async handleTask() {
  try {
    await this.doSomething();
  } catch (error) {
    this.logger.error('任务执行失败', error);
    // 可以发告警通知
  }
}
Note

@Cron()@Interval()@Timeout() 装饰器会自动包裹 try-catch,异常会输出到控制台。但最好自己处理,做点额外的事情比如发告警。

避免重叠执行

任务执行时间超过间隔时间,会导致多个实例同时运行:

@Injectable()
export class TasksService {
  private isRunning = false;

  @Cron(CronExpression.EVERY_MINUTE)
  async handleTask() {
    if (this.isRunning) {
      this.logger.warn('任务正在执行,跳过本次');
      return;
    }

    this.isRunning = true;
    try {
      await this.doSomething();
    } finally {
      this.isRunning = false;
    }
  }
}

或者用 waitForCompletion 选项:

@Cron('* * * * * *', {
  waitForCompletion: true,
})
async handleTask() {
  // 上次没执行完,这次不会启动
  await this.doSomethingLong();
}

分布式锁

多实例部署时,同一个任务只应该在一个实例上执行。用 Redis 分布式锁:

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class DistributedTaskService {
  constructor(private redisService: RedisService) {}

  @Cron('0 */5 * * * *')
  async handleDistributedTask() {
    const lockKey = 'task:lock';
    const acquired = await this.redisService.acquireLock(lockKey, 300);

    if (!acquired) {
      return; // 其他实例在执行,跳过
    }

    try {
      await this.doSomething();
    } finally {
      await this.redisService.releaseLock(lockKey);
    }
  }
}

禁用任务

临时禁用某个任务:

@Cron('0 0 * * * *', {
  disabled: true, // 不会执行
})
handleTask() {
  // 暂时禁用
}

任务管理 API

暴露一个接口来管理定时任务:

import { Controller, Get, Post, Param } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';

@Controller('tasks')
export class TasksController {
  constructor(private schedulerRegistry: SchedulerRegistry) {}

  @Get('cron')
  getCronJobs() {
    const jobs = this.schedulerRegistry.getCronJobs();
    const result = [];
    jobs.forEach((job, name) => {
      result.push({
        name,
        nextDate: job.nextDate()?.toJSDate(),
        running: job.running,
      });
    });
    return result;
  }

  @Post('cron/:name/stop')
  stopCronJob(@Param('name') name: string) {
    const job = this.schedulerRegistry.getCronJob(name);
    job.stop();
    return { message: `任务 ${name} 已停止` };
  }

  @Post('cron/:name/start')
  startCronJob(@Param('name') name: string) {
    const job = this.schedulerRegistry.getCronJob(name);
    job.start();
    return { message: `任务 ${name} 已启动` };
  }
}

这样就能通过 HTTP 接口查看和管理定时任务了。

踩坑经验

坑1:时区问题

服务器是 UTC 时间,业务是东八区。不指定时区的话,任务会在错误的时间执行。一定要用 timeZone 参数明确指定。

坑2:任务执行时间过长

任务还没执行完,下一次调度时间就到了。单实例可以用 waitForCompletion 或手动加锁,多实例必须用分布式锁。

坑3:任务失败没有告警

任务静默失败,过了好久才发现。一定要加错误处理和告警通知。

坑4:重启后任务丢失

动态添加的任务,重启应用就没了。如果需要持久化,得把任务配置存到数据库里,启动时重新加载。

坑5:密集任务影响性能

每秒执行的任务,如果逻辑很重,会拖垮应用。把任务逻辑做轻,或者用消息队列异步处理。

小结

关键知识点回顾:

  • @Cron() 声明 Cron 任务
  • @Interval() 声明间隔任务
  • @Timeout() 声明延迟任务
  • SchedulerRegistry 动态管理任务
  • 生产环境注意错误处理、避免重叠、分布式锁

定时任务是后台处理的核心能力。配合消息队列、分布式锁,能构建健壮的异步处理系统。


恭喜你,到这里 NestJS 教程就全部结束了。从基础概念到高级特性,我们一路走过来,涵盖了 NestJS 开发中最常用的知识点。

记住,框架只是工具,真正的功夫在框架之外。多写代码、多思考、多总结,你也能成为 NestJS 高手。

祝编码愉快!