用 cron 在 Node.js 里实现周期性定时任务
定时任务一开始通常都不复杂。
比如每隔十秒轮询一次接口,用 setInterval 就能写完。可需求一旦变成「每天上午 10 点执行」「周一到周五 9 点半执行」「每月 1 号同步报表」,手写时间判断就会开始绕。
这时候一般会换成 cron 表达式。它不负责让任务变得更可靠,只是把「什么时候跑」这件事写得更像时间规则,而不是一堆日期计算。
Node.js 里可以用 cron 这个包把 cron 表达式和 JavaScript 回调接起来。它适合放一些跟着服务进程一起跑的小任务,比如定时提醒、低频同步、清理缓存、定时拉状态。
有个边界要提前记住:它不是持久化调度系统。进程停了,任务就不会执行;机器重启了,也要等进程重新启动。不能漏的任务,还是要放到系统 crontab、队列、工作流调度器,或者配合 PM2、systemd、Kubernetes CronJob 这类进程管理方案。
什么时候用 cron,什么时候不用
判断会先看它是不是自然时间。
「每隔 5 秒刷新一次内存状态」这类固定间隔任务,用 setInterval 就够。
「每天 10 点」「每周一 9 点」「每月最后一天」这类自然时间任务,用 cron 表达式更顺手。
还有一类任务要单独拎出来:它必须独立于 Node 进程存在,不能因为应用重启就漏掉。这种一般不会只放在应用里的 cron。应用内 cron 更像跟着服务一起活着的小闹钟,不是任务平台。
安装
项目用哪个包管理器,就按哪个来装:
pnpm add cron如果项目还在用 npm 或 yarn,就换成对应命令:
npm install cron
yarn add croncron 表达式怎么读
传统 Unix crontab 是 5 个域,最小粒度是分钟。
cron 这个 Node 包在它前面多加了一个「秒」域,所以常见写法是 6 个域:
秒 分 时 日期 月份 星期
* * * * * *例如:
0 0 10 * * *它的意思是每天 10:00:00 执行。
这 6 个域的范围是:
| 域 | 取值 |
|---|---|
| 秒 | 0-59 |
| 分钟 | 0-59 |
| 小时 | 0-23 |
| 日期 | 1-31 |
| 月份 | 1-12,也可以写 JAN-DEC |
| 星期 | 0-7,0 和 7 都表示星期日,也可以写 MON-SUN |
最常用的特殊写法只有几个:
| 写法 | 含义 | 示例 |
|---|---|---|
* |
任意值 | * * * * * * 表示每秒 |
, |
枚举多个值 | 0 0 10,14,16 * * * 表示每天 10 点、14 点、16 点 |
- |
范围 | 0 0 9-17 * * * 表示每天 9 点到 17 点整点 |
/ |
步长 | 0 */15 * * * * 表示每 15 分钟 |
网上很多 cron 资料会混进 ?、L、W、# 这类 Quartz cron 写法。不同库支持的方言不完全一样,写 Node 代码时别把它们直接搬过来,先看当前库支持哪些语法。
常用例子
日常小任务里,最常用的其实就这些:
| 表达式 | 含义 |
|---|---|
* * * * * * |
每秒执行 |
0 * * * * * |
每分钟的第 0 秒执行 |
0 */5 * * * * |
每 5 分钟执行 |
0 0 10 * * * |
每天 10:00 执行 |
0 30 9 * * MON-FRI |
每周一到周五 9:30 执行 |
0 0 12 1 * * |
每月 1 号 12:00 执行 |
这里最容易写错的是每天 10 点:
* * 10 * * *这不是每天 10 点执行一次,而是每天 10 点这一整小时里每秒执行一次。因为秒和分钟都是 *。
每天 10 点只执行一次,要把秒和分钟都固定下来:
0 0 10 * * *基本用法
最小代码可以这样写:
import { CronJob } from 'cron';
const job = new CronJob(
'0 0 10 * * *',
() => {
console.log('每天 10:00 执行一次');
},
null,
true,
'Asia/Shanghai'
);第四个参数是 start。传 true 时,任务创建后会自动启动;不传或传 false 时,再手动启动:
job.start();停止任务:
job.stop();查看下一次执行时间:
console.log(job.nextDate().toISO());参数一多,构造函数就不太好读。现在更愿意用 CronJob.from(),少一点数参数的负担:
import { CronJob } from 'cron';
const job = CronJob.from({
cronTime: '0 0 10 * * *',
timeZone: 'Asia/Shanghai',
start: true,
waitForCompletion: true,
errorHandler(error) {
console.error('定时任务执行失败', error);
},
async onTick() {
await sendDailyMessage();
}
});这里会额外留意两个参数。
timeZone 用来指定时区。线上任务最好显式写出来,不要依赖服务器默认时区。
waitForCompletion 用来避免上一次异步任务没结束时又启动下一次。如果任务可能跑得比间隔更久,这个选项很有用。
一个真实一点的例子
假设有一个机器人,每天上午 10 点往群里发一条消息,可以这么写:
import { CronJob } from 'cron';
import Bot from './bot';
const bot = new Bot();
const job = CronJob.from({
cronTime: '0 0 10 * * *',
timeZone: 'Asia/Shanghai',
start: true,
waitForCompletion: true,
async onTick() {
await bot.send({
msgType: 'text',
text: { content: '在干嘛' },
at: { atMobiles: ['13xxxxxxxxx', '13xxxxxxxxx'] }
});
},
errorHandler(error) {
console.error('群消息定时任务失败', error);
}
});这段代码只解决了「每天 10 点触发」。如果这条消息不能漏,还得继续补几件事:
- 进程管理:确保服务崩了会自动重启。
- 日志:记录每次任务开始、成功、失败的时间。
- 幂等:重复执行时不要产生错误结果。
- 补偿:如果服务在 10 点刚好不可用,恢复后是否需要补发。
cron 表达式只负责描述时间。任务能不能扛住失败,还得看日志、幂等、补偿和进程管理这些东西有没有补上。