Nestjs框架: nestjs-schedule模块注册流程,源码解析与定时备份数据库
概述
- 接触过Linux的小伙伴,应该知道Linux系统上有定时任务,而Windows系统上叫做计划任务
- 不论哪种系统,大家或多或少都了解过定时任务,它可以通过系统脚本实现
- 基于 NestJS定时任务,确实可以通过系统级脚本实现对MongoDB数据库的维护
- 在管理的是日志部分的数据库,数据库需要进行滚动和备份操作, 就需要定时任务
- NestJS的定时任务功能,有以下两个优点:
- 不受平台限制
- 它跟随业务系统运行,只要平台上有Node.js环境
- 业务系统就能运行,定时任务也能生效
- 支持多种任务类型
- NestJS的定时任务既支持静态定时任务(即随着业务系统启动就注册好)
- 也支持动态定时任务我们可以通过接口动态添加定时任务,这里有个定时任务示例叫
addCronJob
addCronJob(name: string, seconds: string) {const job = new CronJob(`${seconds} * * * * *`, () => {this.logger.warn(`time (${seconds}) for job ${name} to run!`);});this.schedulerRegistry.addCronJob(name, job);job.start();this.logger.warn(`job ${name} added for each minute at ${seconds} seconds!`,); }
- 不受平台限制
场景
- 定时任务有很多应用场景,比如计划类型的业务和通知类型的业务
- 例如,我们每天要检查系统中的计划是否按照用户设定的节点推进,如果没有推进,就可以添加定时任务,每隔一段时间给用户发送通知
- 当然,这里设置的定时任务通常是批量操作,而非为每个用户单独创建。添加定时任务后,其逻辑可能是根据用户数据库关联表格查询对应字段,然后给用户发送通知或消息
配置
下面来实操NestJS定时任务,并针对当前应用场景完成数据库部分数据的定时备份、滚动和清理
1 ) 配置NestJS服务端内容
-
安装依赖:我们需要安装一个名为
@nestjs/schedule
的依赖 -
配置定时任务相关内容
- 我们需要设置一个环境变量 在 .env 类似相关文件中设置
CRON_ON
为true
CRON_ON=true
- 在app.module.js模块或其他拆分管理的module模块中注册:
import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule';@Module({imports: [ScheduleModule.forRoot()], }) export class AppModule {}
- 这样,是全局生效,我们也可以根据配置来根据配置进行条件生效,
- 如, 新建
conditional.module.ts
文件来做拆分管理import { Module } from '@nestjs/common'; import * as dotenv from 'dotenv'; import { toBoolean } from '../utils/format'; import { MailModule } from './mail/mail.module'; import { ScheduleModule } from '@nestjs/schedule'; import { TasksService } from '@/common/cron/tasks.service'; // import { StorageModule } from './storage/storage. module';const imports =[]; const providers = [];const conditionalImports = () => {const envFilePaths = [`.env.${process.env.NODE_ENV |`development`}`,'.env',];const parsedConfig = dotenv.config({ path:'.env'}).parsed;envFilePaths.forEach((path) => {if (path ==',env') return;constconfig = dotenv.config({path});Object.assign(parsedConfig,config.parsed);});if(toBoolean(parsedConfig['MAIL_ON'])){imports.push(MailModule);}if(toBoolean(parsedConfig['CRON_ON']){imports.push(ScheduleModule.forRoot());providers.push(TasksService);}return imports; };@Module({imports: conditionalImports(),providers, });export class ConditionalModule {}
- 将
conditional.module.ts
加入app.module.js
中
- 我们需要设置一个环境变量 在 .env 类似相关文件中设置
-
创建任务服务类:新建
common/cron
目录,在下面新建一个tasks.service.ts
文件,导出TasksService
类import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule';@Injectable() export class TasksService {@Cron('******')handleCron(){console. log('test');} }
- 使用
@Injectable()
装饰器,可参考官方示例 - 在类中调用
console.log
进行打印,其中cron
是定时表达式,写六个星号代表每秒执行一次
- 使用
-
使用定时任务:在上面的
conditional.module
,将TasksService
添加到providers
数组中,已处理 -
这块,条件模块可以优化
import { Module } from '@nestjs/common'; import * as dotenv from 'dotenv'; import { toBoolean } from '../utils/format'; import { MailModule } from './mail/mail.module'; import { ScheduleModule } from '@nestjs/schedule'; import { TasksService } from '@/common/cron/tasks.service'; // import { StorageModule } from './storage/storage. module';const imports =[]; const providers = []; const exportsService = [];@Module({imports: conditionalImports(),providers, });export class ConditionalModule {static register(): DynamicModule {const envFilePaths = [`.env.${process.env.NODE_ENV |`development`}`,'.env',];const parsedConfig = dotenv.config({ path:'.env'}).parsed;envFilePaths.forEach((path) => {if (path ==',env') return;constconfig = dotenv.config({path});Object.assign(parsedConfig,config.parsed);});if(toBoolean(parsedConfig['MAIL_ON'])){imports.push(MailModule);}if(toBoolean(parsedConfig['CRON_ON']){imports.push(ScheduleModule.forRoot());providers.push(TasksService);exportsService.push(TasksService);}imports.push(conditionalImports());return {module: ConditionalModule,imports,providers,exports: exportsService}} }
-
在 app.module.ts 中使用
ConditionalModule
的时候- 替换为:
ConditionalModule.register()
- 替换为:
2 )源码解析
- NestJS的定时任务注册主要在
onApplicationBootstrap
这个生命周期钩子方法中进行 - 我们可以查看
@nestjs/schedule
的官方源码,在相关文件中 schedule/lib/scheduler.orchestrator.ts - 可以找到
onApplicationBootstrap
方法, 定时任务就在这里注册 - 当应用关闭时,会清除对应的定时器、计时器以及定时任务
onApplicationBootstrap(){this.mountTimeouts();this.mountIntervals();this. mountCron(); }onApplicationShutdown(){this. clearTimeouts();this.clearIntervals();this.closeCronJobs(); }
3 ) 定时任务备份数据库
- 参考文档: backup-and-restore-tools/#deployments
- 定时任务主要有两个
- 连接到 MongoDB 并导出
connections
数据 - 删除已有的
connections
数据
- 连接到 MongoDB 并导出
- 删除数据时,需要删除两部分内容
- 一是当前
connections
中的已备份数据,避免重复备份导致日志文件越来越大 - 二是对比备份时间,删除超过一定天数或规则的先前备份的
connections
数据,实现滚动记录,防止磁盘爆满
- 一是当前
- 注意,这里不考虑集群分片的备份,一般集群会用分布式日志系统,如: ELK
- 更多参考上述文档或请教专业运维,下面仅演示单机
MongoDB 备份与恢复
-
要实现备份,需要了解如何在 Docker 中执行
mongodump
命令 -
在 Docker 中有多个 MongoDB 实例,加入要导出端口为
27017
的 MongoDB 中connections
名为log
的数据 -
执行命令如下:
docker exec -it <容器名称> mongodump --uri=<数据库连接> --collection=log --out=/tmp/<时间戳>-log
-
如:
docker exec -it nestjs-starter-mongo-1 mongodump --uri=mongodb://root:exmaple@localhost:27017/nest-logs --collection=log --out=/tmp/2025-06-06-log
-
上述命令将数据备份到 Docker 容器内的临时目录
/tmp
下 -
可以使用以下命令验证备份是否成功:
docker exec -it <容器名称> ls -la /tmp/<时间戳>-log
-
如:
docker exec -it nestjs-starter-mongo-1 ls -la /tmp
-
若要恢复数据,MongoDB 提供了
mongorestore
命令。执行命令如下:docker exec -it <容器名称> mongorestore --uri=<数据库连接> --nsInclude=<源数据库名称> <指定的备份目录路径>
-
如:
docker exec -it nestjs-starter-mongo-1 mongorestore -uri=mongob:/root:exmaple@localhost:27017/nest-logs --nsInclude="nest-logs.log" /tmp/2025-06-06-log/nest-logs
-
为避免覆盖正在使用的
connection
,恢复时应指定新的connection
名称(文件路径), 以便不影响正在收集的connection
-
例如,从
netlogs
中读取数据并恢复到netlogs.log-2025-06-06
这个新的connection
上,若该connection
不存在则会自动创建docker exec -it nestjs-starter-mongo-1 mongorestore -uri=mongob:/root:exmaple@localhost:27017/nest-logs --nsFrom="nest-logs.log" --nsTo="nest-logs.log-2025-06-06" /tmp/2025-06-06-log/nest-logs
-
也就是,如果需要查看历史备份,通常会将其恢复到一个全新名称的
connection
上,而不是覆盖原有的connection
-
现在,我们使用命令来做,每次都是手动的,还是比较麻烦的,我们想要使用定时任务来做,就需要完善定时任务的脚本
-
这里推荐一个 ssh 的工具: ssh2
- 这里基于文档,可以封装一个 ssh 相关的模块
- 来提供 sshService 服务
- 这里细节不提供
-
现在补充定时任务,编辑
common/cron/tasks.service.ts
示例如下:import { Injectable } from '@nestis/common'; import { Cron } from '@nestjs/schedule'; import { SshService } from '@/utils/ssh/ssh.service';@Injectable() export class TasksService {constructor(private sshService: SshService){}@Cron('* * ** * *')handleCron(){//备份:连接到MongoDB并导出对应的db中的collections的数据//滚动记录:删除已有的collections的数据//1.删除当前collections中的已备份数据//2.之前备份的collections->对比collection备份的时间,如果超过t天/hours的规则,则删除const containerName = 'mongo-mongo-1';const uri='mongodb: //root: example@localhost: 27017/nest-logs';const now = new Date();const collectionName = 'log';const outputPath =`/tmp/logs-${now.getTime()}`;const hostBackupPath = '/srv/logs';const cmd =`docker exec -i ${containerName} mongodump --uri=${uri} --collection=${collectionName} --out=${outputPath}`;const cpCmd =`docker cp ${containerName}:${outputPath} ${hostBackupPath}`;await this.sshService.exec(`${cmd} && ${cpCmd}`).catch((err) => err)await this.sshService.exec(`ls-la ${hostBackupPath}`);const delCmd = `find ${hostBackupPath} -type d -mtime +30 -exec rm-rf {}\\;`;await this.sshService.exec(delCmd).catch((err)=>err);const res = await this.sshService.exec(`ls-la ${hostBackupPath}`);console.log('~ TasksService ~ handleCron ~ res:', res);} }