Express集群环境下node-cron定时任务重复执行问题求解
解决Node.js集群环境下node-cron定时任务重复执行的问题
问题场景
使用Express搭建服务器,通过node-cron调度每日10点自动发送邮件的定时任务;为提升性能采用集群技术创建了与CPU核心数相等的8个worker进程,导致定时任务在指定时间执行8次;若将任务移出if-else块,则主进程+8个worker共执行9次,需求是让定时任务仅执行一次。
原代码(任务在worker进程中)
require("dotenv").config(); const express = require("express"); const { errorHandler } = require("./middleware/errorMiddleware"); const connectDB = require("./config/db"); const cors = require("cors"); const cluster = require("cluster"); const totalCPUs = require("os").cpus().length; const process = require("process"); var cron = require('node-cron'); const port = process.env.PORT || 5000; if (cluster.isMaster) { // Fork workers. for (let i = 0; i < totalCPUs; i++) { cluster.fork(); } // 重启退出的worker cluster.on("exit", (worker, code, signal) => { cluster.fork(); }); } else { connectDB(); const app = express(); const corsOptions = { origin: 'http://localhost:3000', optionsSuccessStatus: 204 }; app.use(cors(corsOptions)) app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use("/api/admin", require("./routes/adminRoutes")); app.use(errorHandler); // 每日10点执行的定时任务 cron.schedule('00 10 * * *', function() { console.log('send email'); }); app.listen(port, () => { console.log(`Running on ${port}`); }); }
此代码会输出8次send email(每个worker执行一次)。
移出if-else后的代码
require("dotenv").config(); const express = require("express"); const { errorHandler } = require("./middleware/errorMiddleware"); const connectDB = require("./config/db"); const cors = require("cors"); const cluster = require("cluster"); const totalCPUs = require("os").cpus().length; const process = require("process"); var cron = require('node-cron'); const port = process.env.PORT || 5000; if (cluster.isMaster) { // Fork workers. for (let i = 0; i < totalCPUs; i++) { cluster.fork(); } // 重启退出的worker cluster.on("exit", (worker, code, signal) => { cluster.fork(); }); } else { connectDB(); const app = express(); const corsOptions = { origin: 'http://localhost:3000', optionsSuccessStatus: 204 }; app.use(cors(corsOptions)) app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use("/api/admin", require("./routes/adminRoutes")); app.use(errorHandler); app.listen(port, () => { console.log(`Running on ${port}`); }); } // 每日10点执行的定时任务 cron.schedule('00 10 * * *', function() { console.log('send email'); });
此代码会输出9次send email(主进程+8个worker各执行一次)。
可行解决方案
方案1:仅在主进程中执行定时任务
将定时任务放在cluster.isMaster的分支里,只有主进程会初始化并执行该任务,worker进程不参与定时任务逻辑。
修改后的代码:
require("dotenv").config(); const express = require("express"); const { errorHandler } = require("./middleware/errorMiddleware"); const connectDB = require("./config/db"); const cors = require("cors"); const cluster = require("cluster"); const totalCPUs = require("os").cpus().length; const process = require("process"); var cron = require('node-cron'); const port = process.env.PORT || 5000; if (cluster.isMaster) { // 仅主进程执行定时任务 cron.schedule('00 10 * * *', function() { console.log('send email'); // 这里添加发送邮件的逻辑 }); // Fork workers. for (let i = 0; i < totalCPUs; i++) { cluster.fork(); } // 重启退出的worker cluster.on("exit", (worker, code, signal) => { cluster.fork(); }); } else { connectDB(); const app = express(); const corsOptions = { origin: 'http://localhost:3000', optionsSuccessStatus: 204 }; app.use(cors(corsOptions)) app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use("/api/admin", require("./routes/adminRoutes")); app.use(errorHandler); app.listen(port, () => { console.log(`Running on ${port}`); }); }
原理:主进程只负责管理worker和执行定时任务,worker专注处理HTTP请求,避免了多进程重复执行任务。
方案2:使用分布式锁(基于Redis)
如果担心主进程挂掉导致定时任务失效,可以用Redis实现分布式锁,每个进程在任务触发时尝试获取锁,只有成功拿到锁的进程才执行任务。
步骤:
- 安装Redis依赖:
npm install redis - 修改定时任务逻辑:
// 引入Redis客户端 const redis = require('redis'); const client = redis.createClient(); client.connect().catch(console.error); // 定时任务逻辑 cron.schedule('00 10 * * *', async function() { const lockKey = 'daily-email-lock'; const lockValue = 'locked'; // 设置锁,过期时间5分钟(避免死锁) const lockAcquired = await client.set(lockKey, lockValue, { NX: true, EX: 300 }); if (lockAcquired) { try { console.log('send email'); // 执行发送邮件的逻辑 } finally { // 释放锁 await client.del(lockKey); } } });
原理:利用Redis的SET NX命令实现互斥锁,同一时间只有一个进程能获取到锁,从而保证任务仅执行一次;即使主进程挂了,其他worker进程也能竞争锁执行任务,提升容错性。
方案3:将定时任务独立为单独服务
把定时任务从Express集群服务中分离出来,单独部署一个独立的Node.js进程,完全和Web服务解耦。
示例结构:
project/ ├── server.js # Express集群服务 ├── cron-job.js # 独立的定时任务服务
cron-job.js代码:
require("dotenv").config(); var cron = require('node-cron'); // 每日10点执行的定时任务 cron.schedule('00 10 * * *', function() { console.log('send email'); // 添加发送邮件的逻辑 });
启动方式:分别启动Web服务和定时任务服务:
node server.js node cron-job.js
原理:定时任务单独运行在一个进程中,彻底避免多进程重复执行的问题,同时便于单独维护和扩展。
内容的提问来源于stack exchange,提问作者Roshan Shetty
相关产品推荐
相关产品推荐

