定时任务与后台作业
问题
服务端如何实现定时任务和后台作业?有哪些常见方案?
答案
定时任务方案
| 方案 | 适用场景 | 优缺点 |
|---|---|---|
node-cron | 单实例定时 | 简单,不支持分布式 |
| BullMQ Repeat | 分布式定时 | 基于 Redis,支持集群 |
| @nestjs/schedule | NestJS 项目 | 装饰器语法,优雅 |
| 系统 Crontab | Linux 定时 | 与应用解耦 |
| 云服务 | Serverless 定时 | 免运维 |
Cron 表达式
┌────────── 秒 (0-59) [可选]
│ ┌──────── 分 (0-59)
│ │ ┌────── 时 (0-23)
│ │ │ ┌──── 日 (1-31)
│ │ │ │ ┌── 月 (1-12)
│ │ │ │ │ ┌ 周 (0-7, 0和7都是周日)
│ │ │ │ │ │
* * * * * *
常用示例:
0 * * * *— 每小时整点0 0 * * *— 每天零点0 9 * * 1-5— 工作日早 9 点*/5 * * * *— 每 5 分钟
BullMQ 定时任务
scheduled-jobs.ts
import { Queue, Worker } from 'bullmq';
const taskQueue = new Queue('scheduled-tasks', { connection: redis });
// 1. 可重复任务
await taskQueue.add('daily-report', { type: 'daily' }, {
repeat: { pattern: '0 9 * * *' }, // 每天 9 点
});
await taskQueue.add('cleanup', { type: 'cleanup' }, {
repeat: { every: 60 * 60 * 1000 }, // 每小时
});
// 2. 延迟任务
await taskQueue.add('send-reminder', { userId: '123' }, {
delay: 24 * 60 * 60 * 1000, // 24 小时后执行
});
// 3. 消费者
const worker = new Worker('scheduled-tasks', async (job) => {
switch (job.name) {
case 'daily-report':
await generateDailyReport();
break;
case 'cleanup':
await cleanupExpiredData();
break;
case 'send-reminder':
await sendReminder(job.data.userId);
break;
}
}, { connection: redis });
分布式定时任务注意事项
避免重复执行
多个服务实例同时运行时,定时任务可能被重复执行。需要确保:
- 使用基于 Redis 的任务队列(BullMQ 自动处理)
- 或使用分布式锁保证只有一个实例执行
distributed-cron.ts
// 用分布式锁防止重复执行
async function runDailyReport() {
const lock = await redis.set('lock:daily-report', '1', 'EX', 3600, 'NX');
if (!lock) return; // 其他实例已在执行
try {
await generateDailyReport();
} finally {
await redis.del('lock:daily-report');
}
}
常见面试问题
Q1: 定时任务和消息队列的区别?
答案:
- 定时任务:基于时间触发,如每天凌晨清理数据
- 消息队列:基于事件触发,如用户下单后发送通知
两者经常配合使用:定时任务触发 → 消息队列执行。
Q2: 如何保证定时任务的可靠性?
答案:
- 持久化:任务信息持久化到 Redis/数据库
- 失败重试:自动重试 + 指数退避
- 监控告警:任务未按时执行时告警
- 幂等性:重复执行不产生副作用
Q3: 前端有哪些"定时"场景?
答案:
setInterval轮询requestAnimationFrame动画- Web Worker 后台定时
- Service Worker 后台同步
- cron 触发 ISR 重新构建
相关链接
- 消息队列 - BullMQ 详解
- NestJS 框架深入 - @nestjs/schedule