Nestjs框架: nestjs-schedule模块注册流程,源码解析与定时备份数据库

发布于:2025-06-07 ⋅ 阅读:(18) ⋅ 点赞:(0)

概述

  • 接触过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_ONtrue
      • 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
  • 创建任务服务类:新建 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 数据
  • 删除数据时,需要删除两部分内容
    • 一是当前 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);
    	}
    }
    

网站公告

今日签到

点亮在社区的每一天
去签到