🔥告别 20 分钟等待!NestJS 生产级消息队列 BullMQ 实践指南

🎯 背景:为什么要使用消息队列?

您的业务中是否也有处理时间长达 30 分钟甚至更久的"生成报告"、"大数据分析"或"文件导出"等耗时任务?

传统同步处理的弊端:

  • 糟糕的用户体验: 用户请求长时间阻塞,浏览器可能超时,导致用户必须干等着或反复尝试。
  • 服务器资源浪费: Web 服务器(如 Node.js Event Loop)被长时间占用,无法响应其他用户的快速请求。
  • 请求失败风险高: 任何网络波动或服务器重启都可能导致任务中断,功亏一篑。

我们的解决方案: 异步任务处理

  1. 立即响应: 收到请求后,后端应立即返回"任务已创建,请稍后查询结果"。
  2. 异步解耦: 将耗时任务从主请求线程剥离,交给后台队列(BullMQ Worker)默默处理。
  3. 最终通知/更新: 任务执行完成后,Worker 自动修改数据库状态,完成整个流程。

💡 技术栈:

  • 后端框架:NestJS(TypeScript优先的企业级Node.js框架)
  • 消息队列:BullMQ(基于Redis的高性能消息队列,专为Node.js优化)
  • 依赖工具:ioredis(Redis客户端,用于BullMQ的连接支撑)

BullMQ 的优势: 基于高性能 ioredis 实现,拥有自动重试、失败处理、Job 状态跟踪等生产级功能,并且与 NestJS 的集成优雅方便。

官网BullMQ链接:nest.nodejs.cn/techniques/...

先看下整体结构和结果

文件结构

结果

🛠️ 1. 项目初始化:安装 BullMQ 与配置 Redis

BullMQ 依赖于 Redis 来存储队列信息和任务状态。

1.1 安装所需依赖

bash 复制代码
pnpm install @nestjs/bullmq bullmq ioredis

1.2 配置 Redis 连接信息

在你的 .env 文件中配置 Redis 地址。这是 BullMQ Worker 和 Queue 共享的连接信息。

代码段

ini 复制代码
# redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379

🏭 2. 核心模块:创建 BullMQ 队列与处理器

创建一个专用于消息队列的模块 QueueModule

2.1 配置 QueueModule:连接 Redis 和注册队列

配置 NestJS 的 BullModule.forRootAsync 来连接 Redis,并使用 BullModule.registerQueue 注册一个名为 shortUrl_queue 的队列。

queue.module.ts

js 复制代码
    import { Module } from '@nestjs/common';
    import { ConfigModule, ConfigService } from '@nestjs/config';
    import { BullModule } from '@nestjs/bullmq';
    import { ShortUrlProcessor } from './short-url.processor';
    import { QueueService } from './queue.service';

    @Module({
      imports: [
        // 1. 全局配置 BullMQ:连接到 Redis
        BullModule.forRootAsync({
          imports: [ConfigModule],
          useFactory: async (configService: ConfigService) => ({
            connection: {
              host: configService.get<string>('REDIS_HOST'),
              port: configService.get<number>('REDIS_PORT'),
            },
          }),
          inject: [ConfigService],
        }),
        // 2. 注册具体的队列:命名为 'shortUrl_queue'
        BullModule.registerQueue({
          name: 'shortUrl_queue',
        })
      ],
      providers: [
        QueueService,
        ShortUrlProcessor, // 注册处理器 Worker
      ],
      // ⚠️ 必须导出 BullModule,才能在其他业务模块中使用 @InjectQueue
      exports: [BullModule]
    })
    export class QueueModule { }

2.2 Processor:编写真正的耗时任务逻辑

处理器(Worker)是真正执行任务的地方。它会监听特定队列,并执行 process 方法。

short-url.processor.ts

js 复制代码
    import { OnWorkerEvent, Processor, WorkerHost } from "@nestjs/bullmq";
    import { Job } from "bullmq"; // 引入 Job 类型

    // @Processor('队列名称'): 告知 NestJS 此 Worker 监听哪个队列
    @Processor('shortUrl_queue')
    export class ShortUrlProcessor extends WorkerHost {
      constructor() {
        super();
      }
      
      // 任务处理逻辑:这里可以包含数据库操作、文件处理等耗时任务
      async process(job: Job) {
        console.log(`==== 任务 ID: ${job.id},开始执行消息队列 ====`);
        try {
          const data = job.data;
          console.log('任务接收到的数据: ', data);
          
          // ----------------------------------------------------
          // 核心逻辑:这里模拟 30 分钟甚至更长的耗时任务
          // ----------------------------------------------------
          await new Promise(resolve => setTimeout(resolve, 3 * 1000)); // 实际应为 30 分钟以上的业务逻辑
          
          // TODO: 任务完成后,在这里修改数据库中任务的状态(例如:已完成,并更新结果链接)
          console.log('==== 消息队列执行完毕,修改数据库状态 ====');
          
          // process 必须返回结果,否则 BullMQ 会认为任务失败
          return { status: 'success', message: '任务成功完成' };
        } catch (error) {
          console.log('==== 消息队列执行失败 ====', error);
          // 抛出错误或返回失败信息,BullMQ 会根据配置进行重试
          throw new Error('任务执行过程中出错');
        }
      }

      // 可选:监听任务完成事件 (可用于日志记录)
      @OnWorkerEvent('completed')
      onCompleted(job: Job, result: any) {
        console.log(`[成功] 任务 ${job.id} 完成。`);
      }

      // 可选:监听任务失败事件 (可用于发送告警通知)
      @OnWorkerEvent('failed')
      onFailed(job: Job, err: any) {
        console.log(`[失败] 任务 ${job.id} 失败,原因: ${err.message}`);
      }
    }

🚀 3. 业务集成:将耗时任务推送到队列

现在,我们在业务 Controller 中使用 BullModule 提供的工具将任务推送到队列。

3.1 导入 QueueModule

在需要使用队列的业务模块中导入 QueueModule

js 复制代码
    import { QueueModule } from '../queue/queue.module';

    @Module({
      imports: [
        QueueModule, // 导入我们创建的队列模块
      ],
    })
    export class HongguoModule { }

3.2 在 Controller 中注入并推送任务

在 Controller 中,我们使用 @InjectQueue('队列名称') 注入 BullMQ 的 Queue 实例,然后调用 queue.add() 方法。

hongguo.controller.ts

js 复制代码
    import { InjectQueue } from '@nestjs/bullmq';
    import { Queue } from 'bullmq';
    // ... 其他 NestJS 装饰器

    @Controller('hongguo')
    export class HongguoController {
      constructor(
        private readonly hongguoService: HongguoService,
        // 注入我们注册的 'shortUrl_queue' 队列实例
        @InjectQueue('shortUrl_queue')
        private taskQueue: Queue,
      ) { }

      @ApiOperation({ summary: '长耗时任务 API' })
      @Post('task')
      async createTask(@Body() params: any) {
        // 1. 业务逻辑:先在数据库创建一条任务记录,状态为"待处理"
        const taskRes = { id: 111, status: 'PENDING' } 
        
        // 2. 将任务推送到消息队列
        await this.taskQueue.add(
          'shortUrl-process', // 队列中任务的唯一名称 (Job name)
          { taskId: taskRes.id, inputParams: params }, // 传递给处理器的实际数据
          {
            removeOnComplete: true, // 任务成功完成后自动清除 Job 记录
            removeOnFail: false,   // 任务失败后保留,方便调试和手动重试
            attempts: 3, // 失败后最多重试 3 次
          }
        );

        // 3. 立即返回响应给前端
        return {
          data: taskRes,
          msg: '✅ 任务已创建,后台正在异步处理中,请稍后查询结果'
        };
      }
    }
    

💡 效果与结果展示

场景 同步处理 异步处理(BullMQ)
API 响应时间 > 30 分钟(或超时) < 100ms,立即返回
服务器负载 Event Loop 阻塞,吞吐量骤降 主线程无阻塞,可继续处理其他请求
任务可靠性 中断即丢失 支持重试、持久化、失败告警
用户体验 卡顿、焦虑、反复刷新 流畅、可轮询状态、体验友好

🚀 总结与进阶

通过 NestJS + BullMQ ,我们成功将长耗时任务从主线程剥离,实现了高可用、可扩展、易维护的异步架构。

进阶思考:

  1. Job 状态查询: 如何让用户实时查询任务进度?

    • 可以通过 BullMQ 的 taskQueue.getJob(jobId) 方法结合 WebSocket 实现实时进度推送。
  2. 可视化监控: 生产环境需要一个面板来监控队列健康和失败任务。

    • 可以集成 [Bull Board] 等工具,提供友好的 Web UI 来管理和查看 BullMQ 队列状态。
相关推荐
疯狂的程序猴1 小时前
打包生成的苹果APP上架到苹果官方appstore商店的详细流程与教程
后端
该用户已不存在1 小时前
Google Antigravity 测评:免费的智能体 IDE 有多强
后端·ai编程·gemini
狗头大军之江苏分军1 小时前
【压力】一位一线炼钢工人的消失
前端·后端
00后程序员1 小时前
如何抓包 (How to Capture Packets) - 完整指南与实用技巧
后端
回家路上绕了弯1 小时前
日增千万数据:数据库表设计与高效插入存储方案
分布式·后端
拉不动的猪2 小时前
文件下载:后端配置、前端方式与进度监控
前端·javascript·浏览器
j***89462 小时前
Spring Boot整合Redisson的两种方式
java·spring boot·后端
FreeCode2 小时前
LangGraph智能体开发快速入门
后端·langchain·agent
Code_Artist2 小时前
robfig/cron定时任务库快速入门
分布式·后端·go