BullMQ 实战:Node.js 定时任务与任务队列完全指南
BullMQ 是 Node.js 生产级任务队列:Redis 持久化、失败重试、cron 定时、并发控制与水平扩展。本文从入门到生产:任务创建、cron 表达式、队列管理、Worker 配置与监控面板。
BullMQ 用 Redis 存储任务,任务能否在故障后恢复取决于 Redis 持久化与高可用配置,支持 cron 定时、延迟执行、失败重试和并发控制。核心三件套:Queue 负责入队、Worker 负责消费、QueueEvents 负责监听状态;多个 Worker 通过 Redis 协调;故障恢复时任务可能再次执行,业务必须保证幂等。
为什么是 BullMQ
图片处理、外部 API 调用、备份、邮件推送——这些耗时操作不该阻塞 Web 请求。BullMQ 把它们卸载到后台:Redis 保存队列状态,需另行配置持久化与恢复策略,重试机制吸收瞬时故障,水平扩展让你按需增加 Worker 实例。适用于需要持久队列和后台消费者的 Node.js 服务。
安装与基本结构
npm install bullmq ioredis
// 保存为 .mjs;db 和 sendWelcomeEmail 由应用提供
import { Queue, Worker } from 'bullmq';
const connection = { host: 'localhost', port: 6379 };
// 生产者:入队
const emailQueue = new Queue('email', { connection });
await emailQueue.add('welcome', { userId: 123 });
// 消费者:处理
const worker = new Worker('email', async job => {
const user = await db.user.find(job.data.userId);
await sendWelcomeEmail(user.email);
}, { connection, concurrency: 5 });
job.data 只放可序列化的基本数据(传 ID 不传对象),与所有任务队列的铁律一致。
定时与延迟任务
// cron:每天凌晨 2 点备份
await backupQueue.upsertJobScheduler(
'daily-backup',
{ pattern: '0 2 * * *', tz: 'Asia/Shanghai' },
{ name: 'daily-backup', data: {} }
);
// 延迟 10 分钟执行一次
await emailQueue.add('reminder', { userId: 123 }, { delay: 10 * 60 * 1000 });
// 指定时间点执行一次
await queue.add('report', data, { delay: runAt.getTime() - Date.now() });
BullMQ 5.16+ 使用 Job Scheduler;相同调度器 ID 可更新配置,但任务处理仍需幂等。延迟任务还受 Worker 可用性影响,不是严格的实时执行承诺。
任务定制:重试、优先级与去重
await queue.add('api-sync', data, {
attempts: 5, // 最多尝试 5 次,含首次执行
backoff: { type: 'exponential', delay: 3000 }, // 指数退避
priority: 10, // 正优先级中数字越小越高;未设优先级的任务优先于这些任务
jobId: `sync-${data.accountId}`, // 固定 jobId 实现去重
removeOnComplete: 100, // 只保留最近 100 条完成记录
removeOnFail: 500
});
同一队列内任务记录仍存在时,jobId 可避免重复入队;记录删除后相同 ID 可再次入队,不能替代业务幂等。removeOnComplete/Fail 控制 Redis 内存膨胀——这两个选项不配好,队列历史会吃掉全部内存。
Worker 管理与优雅停机
Worker 配置项关键点:concurrency 控制单实例并发数;limiter 按 max/duration 限制同一队列全体 Worker 启动任务的速率,不直接限制任务内部的 HTTP 请求数。优雅停机:
process.on('SIGTERM', async () => {
await worker.close(); // 停止取新任务,等待手头任务完成
process.exit(0);
});
容器滚动发布时的标配;还需给足终止宽限时间,并关闭 Queue/QueueEvents 等连接,超时或强杀仍可能中断任务。
监控:Bull Board 与生产方案
// 与上面的 .mjs 示例一致;app 是已配置的 Express 实例
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');
createBullBoard({
queues: [new BullMQAdapter(emailQueue)],
serverAdapter
});
app.use('/admin/queues', serverAdapter.getRouter());
Bull Board 提供队列状态面板。生产环境必须在 /admin/queues 前配置认证与授权,不能把任务操作界面公开暴露。
从队列等待到任务执行定位延迟
BullMQ 的 Redis 连接正常,不代表任务能及时执行。应用应分别上报等待时长、执行时长、失败和重试次数,并在日志中记录 queue、job_id 与处理结果。观测云的 Redis 采集用于检查内存和连接压力,队列深度和等待时间则需由 BullMQ API 或自定义指标提供。任务调用链需要兼容的插桩或手动 Span;跨入队、消费进程的上下文也要显式传递。
接入文档:Redis 采集 · 日志采集 · Node.js APM 接入 · 监控器与告警策略。
常见问题(FAQ)
Q:BullMQ 和 Agenda 怎么选?
项目已有 Redis 选 BullMQ(功能最全、社区最活跃);已有 MongoDB 且不想加中间件选 Agenda。新项目的默认答案是 BullMQ。
Q:任务执行中进程崩溃会怎样?
BullMQ 有 stalled 检测:Worker 未能续期处理锁的任务会被检测为 stalled,并按 maxStalledCount 等配置重新入队或失败。前提依然是任务幂等——重复执行不应产生副作用。
Q:定时任务在 Worker 进程还是主进程注册?
用稳定 ID 调用 upsertJobScheduler 更新调度定义;不要将先 remove 再 add 当成原子 upsert。旧版 repeat API 的迁移应按所用版本文档进行。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。