Node.js 每日摘要邮件发了两遍:我用 Redis 锁和幂等表堵住集群重复任务
PM2 多实例部署后,定时任务不再天然只跑一次。我用 Redis 分布式锁减少并发执行,再用 MongoDB 幂等表保证邮件不重复,最后靠对账补发兜底漏发。

作者
ZHOUYI
上周运营同事发来截图:同一封每日摘要,用户在邮箱里看到了两遍。排查后发现服务用 PM2 cluster 起了多个实例,定时任务写在应用里,每个实例到点都会触发。单实例时没问题,上集群后相当于多个节点同时抢同一个任务,邮件自然重复。
先用 Redis 分布式锁挡住并发触发
第一层处理是加分布式锁:任务触发时,用 Redis SET key token NX PX 抢锁,只有抢到的实例继续执行。不要分开执行 SETNX 和 EXPIRE,避免加锁后进程崩溃导致死锁。释放锁时也要带随机 token,用 Lua 判断后再删,防止误删别的实例刚拿到的锁。
const token = crypto.randomUUID();
const ok = await redis.set(lockKey, token, 'PX', ttl, 'NX');
if (!ok) return;
try {
await sendDailyDigest(date);
} finally {
const script = `if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end`;
await redis.eval(script, 1, lockKey, token);
}再用发送幂等表保证结果唯一
只靠锁不够:锁过期、Redis 抖动、任务失败重试,都可能让第二个执行者进来。所以我加了发送幂等表。以「任务类型 + 业务日期」生成幂等键,例如 daily_digest:2026-06-22,在 MongoDB 里建唯一索引。执行前先 insert 一条 pending 记录,插入成功才发送;如果报重复键,说明已有实例处理过,直接跳过。
- 幂等键按业务日期生成,而不是按任务触发时间,避免跨天补发混乱。
- 记录状态用 pending、success、failed,失败可重试,成功后不再重发。
- 如果是用户维度摘要,可以再加 userId 或批次号,避免局部补发影响全量。
补发和对账:漏发也不能重复打扰
补发逻辑也要幂等。每天任务跑完后,我会用对账脚本比对发送记录和幂等表:只把 failed 或超时未完成的批次放进补发队列。补发前仍然检查幂等键和状态,确保不会把已成功用户再打扰一遍。这样即使某次锁没挡住,也不会产生第二封邮件。
“这套组合里,Redis 锁负责减少并发执行,幂等表负责保证结果唯一,对账负责兜底漏发。定时任务上集群后,不能再相信“只会跑一次”,要把重复执行当成默认前提来设计。”