You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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实现分布式锁,每个进程在任务触发时尝试获取锁,只有成功拿到锁的进程才执行任务。

步骤:

  1. 安装Redis依赖:npm install redis
  2. 修改定时任务逻辑:
// 引入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 08:56:08