node-schedule定时任务重复执行:生产环境邮件重复发送问题排查
生产环境下node-schedule重复发送邮件问题排查
我是Web开发新手,开发了一款支持用户创建群组、开展每周视频会议的Web应用。通过node-schedule实现了两个核心功能:
- 会议前24小时向用户发送提醒邮件
- 会议结束后发送调研邮件
该功能在测试环境运行完全正常,但部署到生产环境后,每位用户都会收到2份提醒邮件和2份调研邮件。
设计逻辑
父定时任务每天凌晨0点执行,筛选出次日有会议的群组,为其调度两个子任务:
- 会议前24小时的提醒邮件
- 会议结束(按会议时长计算结束时间)后的调研邮件
举个例子:如果群组1在周二13:30有60分钟会议,周一0点执行的父任务会调度:
- 周一13:30发送提醒邮件
- 周二14:30发送调研邮件
我曾尝试将父调度改为每5分钟执行一次,筛选未来5分钟内需要触发24小时提醒的群组,测试环境功能完全正常,但生产环境依旧出现重复发送的问题。
代码实现
const schedule = require('node-schedule') const { sendMeetingReminder } = require('./meeting.email.helper') const { findAllForEmails } = require('../controllers/groupControllers') const { getNextRecurrenceDate } = require('./meetingRecurrenceDate') const subjectReminder = 'Dein Gruppen-Meeting beginnt morgen!' const subjectFeedback = 'Dein Feedback zum letzten Gruppentreffen bei Groupera' const meetingScheduler = schedule.scheduleJob('0 0 * * *', async () => { let currentDate = new Date() try { const groupsAll = await findAllForEmails() const groups = groupsAll.filter((group) => { const nextMeeting = getNextRecurrenceDate( group.meetings[0], currentDate ) const reminder24Hours = new Date(nextMeeting) reminder24Hours.setHours(reminder24Hours.getHours() - 24) return ( reminder24Hours >= currentDate && reminder24Hours.getTime() < currentDate.getTime() + 23 * 60 * 60 * 1000 ) }) groups.forEach((group) => { const nextMeeting = getNextRecurrenceDate( group.meetings[0], currentDate ) const reminder24Hours = new Date(nextMeeting) reminder24Hours.setHours(reminder24Hours.getHours() - 24) const post1Hour = new Date(nextMeeting) post1Hour.setMinutes( post1Hour.getMinutes() + group.meetings[0].duration ) const reminder = schedule.scheduleJob(reminder24Hours, async () => { if (process.env.NODE_ENV === 'development') { console.log(`sending reminder to ${group.name}`) } else { await sendMeetingReminder( group.moderator.email, group.moderator.alias, group.name, subjectReminder, 24 ) if (group.members.length >= 1) group.members.forEach(async (member) => { await sendMeetingReminder( member.email, member.alias, group.name, subjectReminder, 24 ) }) } reminder.cancel() }) const survey = schedule.scheduleJob(post1Hour, async () => { if (process.env.NODE_ENV === 'development') { console.log(`sending feedback to ${group.name}`) } else { await sendMeetingReminder( group.moderator.email, group.moderator.alias, group.name, subjectFeedback, 1 ) if (group.members.length >= 1) group.members.forEach(async (member) => { await sendMeetingReminder( member.email, member.alias, group.name, subjectFeedback, 1 ) }) } survey.cancel() }) }) } catch (error) { console.error('Error fetching groups and meetings:', error) } }) module.exports = { meetingScheduler }
问题排查与解决方案
1. 生产环境多进程重复执行
生产环境可能存在多个Node.js进程同时运行(比如PM2多实例、Docker容器重复启动),每个进程都会独立调度定时任务,导致同一任务被多次触发。
- 验证:在父任务执行时打印进程ID:
console.log('Parent job running, PID:', process.pid),如果生产环境同一时间出现多个不同PID的日志,说明存在多进程问题。 - 解决:
- 若用PM2,设置单实例运行:
pm2 start app.js -i 1。 - 必须多进程时,使用Redis分布式锁,确保同一时间只有一个进程执行父任务。
- 若用PM2,设置单实例运行:
2. 筛选条件导致重复调度
原筛选条件reminder24Hours >= currentDate && reminder24Hours.getTime() < currentDate + 23h可能让同一群组在连续两天的父任务中都被选中,比如reminder24Hours是当天23:00时,第二天0点的父任务仍会满足条件,导致子任务重复创建。
- 解决:优化筛选条件,确保每个群组的子任务只被调度一次:
// 替换原筛选逻辑 const reminderDate = new Date(reminder24Hours); const currentDateNextDay = new Date(currentDate); currentDateNextDay.setDate(currentDateNextDay.getDate() + 1); // 检查reminder24Hours是否属于次日 return reminderDate.getDate() === currentDateNextDay.getDate() && reminderDate.getMonth() === currentDateNextDay.getMonth() && reminderDate.getFullYear() === currentDateNextDay.getFullYear();
3. 子任务重复创建
父任务重复执行时,可能为同一群组创建多个相同的子任务。可以通过唯一标识避免重复:
- 解决:为每个子任务添加唯一key,创建前检查是否已存在:
// 提醒任务 const reminderJobKey = `reminder_${group.id}_${reminder24Hours.getTime()}`; if (!schedule.scheduledJobs[reminderJobKey]) { const reminder = schedule.scheduleJob(reminderJobKey, reminder24Hours, async () => { // 原任务逻辑 delete schedule.scheduledJobs[reminderJobKey]; }); } // 调研任务同理 const surveyJobKey = `survey_${group.id}_${post1Hour.getTime()}`; if (!schedule.scheduledJobs[surveyJobKey]) { const survey = schedule.scheduleJob(surveyJobKey, post1Hour, async () => { // 原任务逻辑 delete schedule.scheduledJobs[surveyJobKey]; }); }
4. 异步循环潜在问题
forEach中使用async/await可能导致异步执行顺序混乱,改用for...of确保顺序执行,避免潜在的重复调用:
// 替换原members.forEach逻辑 if (group.members.length >= 1) { for (const member of group.members) { await sendMeetingReminder( member.email, member.alias, group.name, subjectReminder, 24 ); } }
内容的提问来源于stack exchange,提问作者John D.
相关产品推荐
相关产品推荐

