在 PM2 集群模式下,未加控制的定时任务可能因多进程并发导致每 3 秒的任务错误执行 8 次,而 2023 年 JavaScript 开发者调查显示 78% 的 Node.js 应用均面临此类调度需求。
原因分析
Node.js 定时任务重复执行的根本原因在于调度器的进程级隔离特性。node-cron 被定义为进程级调度器,不具备跨实例协调能力,无法感知其他实例是否存在相同任务。在 PM2 的 cluster 模式中,每个 worker 进程是同步独立执行的,若实例数为 8,则同一 cron 表达式会在 8 个进程中同时触发。此外,开发者常误以为加锁如内存变量 isRunning 即可解决,却忽视了多进程间内存不共享的本质限制,导致日志中同一任务被多次打印或数据库出现重复记录。
解决方案
方案一:PM2 实例单点执行
针对 PM2 集群部署,最轻量级的方案是限制仅在特定实例 ID 上运行任务。通过获取环境变量 process.env.NODE_APP_INSTANCE,可确保任务只在一个 worker 中启动。具体代码逻辑为:if (process.env.NODE_APP_INSTANCE === '0') { console.log('执行定时任务') }。实测表明,该方法能将每 3 秒打印 8 次的日志恢复为正常的单次执行。
方案二:Redis 分布式锁
在 Kubernetes 或多服务器场景下,需使用 Redis 实现跨进程互斥。利用 Redis 的原子操作 SET key value NX PX 实现锁机制,建议失效时间设置为 20 秒,该时间需大于多台服务器之间的时间差以防任务多次执行。业务代码实现中,通过 redisTemplate.opsForValue().setIfAbsent 判断锁获取状态,只有返回 true 时才执行业务逻辑,执行完毕后依靠过期时间自动释放或手动删除 key。
方案三:任务状态标记法
对于单机多任务防重,可利用 node-cron 的 Job 类提供的 isRunning() 方法。在任务开始执行前检查当前任务状态,若 task.isRunning() 返回 true 则直接退出。此方案无需额外中间件,适用于低频任务,但无法解决多实例部署下的并发问题。
注意事项
第一,Redis 锁的过期时间设置至关重要,若多台服务器时间差大于超时时间,定时任务可能会执行多次,因此失效时间要大于多台服务器之间的时间差。第二,避免使用 setInterval 实现精确定时循环,因其存在定时漂移问题,建议使用递归 setTimeout 可实现更精确的计时。第三,不要依赖 node-schedule 的 lib/Job.js 中 trackInvocation 方法跨进程同步,该项目核心代码集中在 lib/Job.js 和 lib/schedule.js,但仅作用于当前进程内存。
参考来源
来源:CSDN - Node.js 定时任务如何避免多实例重复执行?_编程语言
来源:CSDN - 彻底解决 node-cron 重复执行难题:5 个幂等性设计方案
来源:CSDN - node.js 定时执行 node 定时任务重复执行
来源:CSDN - Node.js 定时任务调度:使用 node-schedule 实现定时触发功能