跳到主要内容

定时任务与后台作业

问题

服务端如何实现定时任务和后台作业?有哪些常见方案?

面试速答版

Node.js 定时任务和后台作业有哪些方案? 按规模和场景选型:

  • node-cron:单进程内跑 Cron 表达式,简单够用,但多实例会重复执行
  • BullMQ:基于 Redis 的任务队列,原生支持 repeat: { pattern: '0 9 * * *' }、延迟任务、重试,分布式场景首选
  • @nestjs/schedule:NestJS 项目用 @Cron('0 0 * * *') 装饰器,本质还是单实例。
  • 系统 Crontab:和应用解耦,适合数据库备份、日志轮转这类运维任务。
  • 云服务AWS EventBridgeCloudflare Cron Triggers,Serverless 场景免运维。

Cron 表达式怎么写?分布式怎么防重复?

  • 常用模式0 * * * * 每小时整、0 0 * * * 每天零点、*/5 * * * * 每 5 分钟、0 9 * * 1-5 工作日早 9 点。
  • 防重复执行:多实例同时跑 node-cron 会执行 N 次,必须用分布式锁(Redis SET NX EX)让一个实例抢到锁才执行,或者直接用 BullMQ(队列天然只消费一次)。
  • 失败重试:BullMQ 配 attempts: 3 + backoff: { type: 'exponential' },失败任务进死信队列人工处理。
  • 任务监控:BullMQ 有 bull-board UI,能看到队列长度、失败任务、重试情况。

答案

定时任务方案

方案适用场景优缺点
node-cron单实例定时简单,不支持分布式
BullMQ Repeat分布式定时基于 Redis,支持集群
@nestjs/scheduleNestJS 项目装饰器语法,优雅
系统 CrontabLinux 定时与应用解耦
云服务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 });

分布式定时任务注意事项

避免重复执行

多个服务实例同时运行时,定时任务可能被重复执行。需要确保:

  1. 使用基于 Redis 的任务队列(BullMQ 自动处理)
  2. 或使用分布式锁保证只有一个实例执行
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: 如何保证定时任务的可靠性?

答案

  1. 持久化:任务信息持久化到 Redis/数据库
  2. 失败重试:自动重试 + 指数退避
  3. 监控告警:任务未按时执行时告警
  4. 幂等性:重复执行不产生副作用

Q3: 前端有哪些"定时"场景?

答案

  • setInterval 轮询
  • requestAnimationFrame 动画
  • Web Worker 后台定时
  • Service Worker 后台同步
  • cron 触发 ISR 重新构建

相关链接