定时任务
本教程共 47 篇 · 第 31 篇 · 更新于 2026-08-09 · 约 13 分钟阅读
本节目标:掌握 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 高手。
祝编码愉快!