🔥告别 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 队列状态。
相关推荐
油丶酸萝卜别吃9 分钟前
修改chrome配置,关闭跨域校验
前端·chrome
锥锋骚年16 分钟前
golang 发送内网邮件和外网邮件
开发语言·后端·golang
m0_7400437324 分钟前
3、Vuex-Axios-Element UI
前端·javascript·vue.js
雨雨雨雨雨别下啦27 分钟前
Spring AOP概念
java·后端·spring
on the way 12327 分钟前
day04-Spring之Bean的生命周期
java·后端·spring
风止何安啊29 分钟前
一场组件的进化脱口秀——React从 “类” 到 “hooks” 的 “改头换面”
前端·react.js·面试
代码笔耕30 分钟前
面向对象开发实践之消息中心设计(二)
java·后端·架构
JS_GGbond30 分钟前
给数组装上超能力:JavaScript数组方法趣味指南
前端·javascript
前端无涯31 分钟前
Tailwind CSS v4 开发 APP 内嵌 H5:安卓 WebView 样式丢失问题解决与降级实战
前端
小邋遢2.033 分钟前
vscod 执行npm build报错:Error: Cannot find module ‘vite‘
前端·npm·node.js