Node.js 定时任务踩坑:setInterval 累积漂移与替代方案
很多 Node.js 项目一上来就用 setInterval 写定时任务,结果跑久了发现执行时间越来越偏,甚至任务重入导致数据错乱。本文结合真实案例讲清 setInterval 的时间漂移原理、任务重入问题,并给出 setTimeout 自校正、node-cron、以及独立进程调度三种更可靠的替代方案,帮你写出稳得住的定时逻辑。
# 一、问题现象
一个每 5 分钟跑一次的数据同步任务,上线两周后开始在凌晨 3 点执行——本该是整点批次。日志里发现执行间隔有时 5 分钟,有时 6 分半,**时间不断往后漂**。
# 二、为什么会漂移
`setInterval(fn, 3000)` 只保证"至少间隔 3 秒",**不保证精确**:
- 事件循环被同步代码阻塞时,回调只能排队等待;
- 回调自身执行耗时会计入下一个周期;
- Node.js 不补偿已经错过的周期。
所以漂移是**结构性**的,不是偶发 bug。
```js
// 漂移示例:每轮都会往后累积
setInterval(() => {
heavySyncTask(); // 假设耗时 30 秒
}, 60000);
```
# 三、任务重入更危险
如果任务耗时超过间隔,会出现**上一次还没跑完、下一次又开始**的并发执行。对写数据库的任务来说,可能造成重复写入或数据竞争。
# 四、三种可靠替代
## 1. setTimeout 自校正(推荐轻量场景)
```js
async function loop() {
const start = Date.now();
try { await task(); } catch (e) { console.error(e); }
const cost = Date.now() - start;
const delay = Math.max(0, INTERVAL - cost);
setTimeout(loop, delay);
}
loop();
```
延迟按"间隔减去执行耗时"动态计算,任务不重入,漂移可控。
## 2. node-cron 处理日历语义
需要"每天凌晨 2 点跑"这类日历规则,用 `node-cron` 更直观:
```js
cron.schedule("0 2 * * *", task, { timezone: "Asia/Shanghai" });
```
务必显式指定 timezone,否则容器里默认 UTC,任务会错开 8 小时。
## 3. 独立进程 + 分布式锁
多实例部署时,任何单机定时器都会**每个实例各跑一遍**。正确做法是外置调度(如系统 cron、K8s CronJob),或加 Redis 分布式锁保证只有一个实例执行。
# 五、避坑清单
1. **别在 setInterval 里放重活**,先算出耗时再决定方案;
2. **加执行锁**,`isRunning` 标志位防止重入;
3. **时区显式声明**,别依赖服务器默认;
4. **多实例必须外置调度**,本机定时器只适合单实例;
5. **日志记录开始和结束**,漂移和重入才有据可查。
# 六、小结
setInterval 不是不能用,而是要清楚它的局限。轻量单实例场景用 setTimeout 自校正就够,复杂日历规则上 node-cron,多实例一定要把调度外包出去。