如何在NestJS中避免PM2集群模式下定时任务的数据库锁问题
解决PM2集群模式下NestJS定时任务重复执行的问题
PM2的cluster模式会启动多个独立的应用进程,每个进程都会初始化NestJS应用实例,包括注册定时任务,因此多个集群节点会同时触发同一定时任务,进而导致数据库锁冲突这类问题。下面提供几种可行的解决思路:
方法一:仅让PM2主进程执行定时任务
PM2在cluster模式下会给每个进程设置NODE_APP_INSTANCE环境变量,主进程的该值为0,其他子进程为1、2...。利用这个变量可以控制只有主进程才执行定时任务。
方式1:在任务方法中判断
import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; @Injectable() export class TasksService { private readonly isMainInstance = process.env.NODE_APP_INSTANCE === '0'; @Cron('0 0 * * *') // 示例:每天凌晨执行 async handleDailyTask() { // 非主进程直接跳过 if (!this.isMainInstance) return; // 你的定时任务逻辑 console.log('主进程执行定时任务'); } }
方式2:全局禁用非主进程的定时任务
在ScheduleModule初始化时直接控制启用状态:
import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { TasksService } from './tasks.service'; @Module({ imports: [ ScheduleModule.forRoot({ enabled: process.env.NODE_APP_INSTANCE === '0', }), ], providers: [TasksService], }) export class TasksModule {}
方法二:使用分布式锁实现任务互斥
如果担心主进程宕机导致定时任务中断,可以用分布式锁(基于Redis、数据库等)确保同一时间只有一个进程能执行任务,适合高可用场景。
以Redis为例的实现:
import { Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { Redis } from 'ioredis'; // 或你使用的Redis客户端 @Injectable() export class TasksService { private readonly redisClient = new Redis({ /* 你的Redis配置 */ }); @Cron('0 0 * * *') async handleDailyTask() { const lockKey = 'task:daily-job:lock'; const lockValue = Date.now().toString(); const lockExpireMs = 60 * 1000; // 锁过期时间,防止死锁 // 尝试获取排他锁 const lockAcquired = await this.redisClient.set(lockKey, lockValue, 'NX', 'PX', lockExpireMs); if (!lockAcquired) { console.log('已有进程在执行任务,本次跳过'); return; } try { // 执行你的任务逻辑 console.log('获取到锁,执行定时任务'); } finally { // 仅释放自己获取的锁,避免误删其他进程的锁 const currentLockValue = await this.redisClient.get(lockKey); if (currentLockValue === lockValue) { await this.redisClient.del(lockKey); } } } }
方法三:分离业务进程与定时任务进程
将定时任务单独部署为一个PM2实例,用fork模式运行(单进程),业务服务继续用cluster模式,通过环境变量控制是否启用定时任务。
修改你的ecosystem.js配置:
module.exports = { apps: [ { name: 'my_app', script: 'dist/main.js', instances: 0, exec_mode: 'cluster', listen_timeout: 10000, kill_timeout: 1000, env: { DISABLE_SCHEDULE: 'true', // 业务进程禁用定时任务 }, }, { name: 'my_app_schedule', script: 'dist/main.js', instances: 1, exec_mode: 'fork', listen_timeout: 10000, kill_timeout: 1000, env: { DISABLE_SCHEDULE: 'false', // 定时任务进程启用任务 }, }, ], };
然后在NestJS中根据环境变量控制定时任务的启用:
ScheduleModule.forRoot({ enabled: process.env.DISABLE_SCHEDULE !== 'true', })
内容的提问来源于stack exchange,提问作者user10874312
相关产品推荐
相关产品推荐

