为什么需要 BullMQ

问题:HTTP 请求不适合承担所有工作

假设用户请求生成一份大型报表:

HTTP 请求 -> 查询大量数据 -> 生成文件 -> 上传文件 -> 发送邮件 -> HTTP 响应

如果全部在请求中同步完成,会出现:

使用 BullMQ 后:

HTTP 请求
    |
    v
Producer --添加任务--> Redis/BullMQ Queue
    |                       |
    v                       v
返回 202 Accepted       Worker 获取并执行
                            |
                            v
                    completed / failed

Web 服务只负责验证请求和提交任务,Worker 独立处理耗时工作。

BullMQ 解决了什么

削平流量高峰

一分钟到来 10,000 个任务,不代表必须在一分钟内同时执行 10,000 个。队列可以积压任务,Worker 按可承受的并发速度消费。

这叫作负载整形或削峰:用更长的处理时间换取系统稳定性。

分离提交和执行

Producer 与 Worker 可以运行在:

生产环境通常将 Web 服务和 Worker 分开部署,避免重任务拖慢 HTTP 接口。

自动重试和延迟

第三方 API 暂时失败时,可以采用退避策略重试,而不是让用户重新提交整个请求。

水平扩展

增加 Worker 实例即可并行消费同一队列。官方文档强调 BullMQ 可以通过增加 Worker 横向扩展。

保存任务状态

任务会经历等待、执行、完成或失败等状态,便于查询进度和排查错误。

Job 的生命周期

根据官方 Architecture,一个普通任务可能经历:

wait / prioritized / delayed
             |
             v
           active
          /      \
         v        v
   completed    failed
                   |
                   +-- retry --> wait

Flow 中存在父子依赖时,父任务还可能进入 waiting-children,等待子任务完成。

理解生命周期很重要,因为“任务已经添加”并不等于“任务已经完成”。

BullMQ 与 Node-Redis 的区别

Node-Redis 是通用 Redis 客户端:

await client.rPush("jobs", JSON.stringify(data));

BullMQ 是任务队列框架,它在 Redis 之上提供:

自己用 LPUSH / BLPOP 实现的只是简单队列,还需要自行解决 Worker 崩溃、重复执行、失败记录、重试和清理等问题。

BullMQ 与消息中间件的区别

BullMQ 很适合 Node.js 应用中的后台任务,但不是所有消息系统的唯一选择。

需求 常见选择
Node.js 后台任务、延迟和重试 BullMQ
复杂跨系统路由、协议互操作 RabbitMQ
大规模事件流、长时间保留和回放 Kafka
云平台深度集成的托管队列 SQS 等云队列
单进程、无需持久化的小任务 内存队列或 Worker Threads

选型应依据交付语义、吞吐量、运维能力和语言生态,而不是只比较 API 是否简单。

任务可能重复执行

BullMQ 首页描述的是尽力实现一次交付,但最坏情况下可能至少一次交付。Worker 崩溃、锁丢失或重试时,同一业务任务可能再次运行。

因此:

任务成功 = 处理函数返回成功
业务安全 = 即使处理函数重复运行,最终结果仍正确

例如发送付款请求时,应使用业务幂等键或数据库唯一约束,不能只相信 Job ID。

适合 BullMQ 的场景

不适合直接使用 BullMQ 的场景

最佳实践起点

本章小结

BullMQ 的核心价值是把任务执行从请求路径中解耦,用 Redis 保存任务状态,并通过 Worker 控制并发、重试和扩展。引入队列并不会自动解决一致性问题;安全的任务仍需要幂等设计、资源限制和故障策略。