概述
- 接触过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); } }