NestJS 系列教程(十七):异步任务与消息队列(Bull + Redis 企业级实战)

🚀 NestJS 系列教程(十七):异步任务与消息队列(Bull + Redis 企业级实战)

✨ 本篇目标

本篇你将学会:

  • 为什么要用消息队列(削峰填谷、解耦、提升响应速度)

  • 在 NestJS 中如何集成 Bull(Redis Queue)

  • 如何实现:

    • Producer(生产任务)
    • Consumer / Processor(消费任务)
    • 重试(attempts + backoff)
    • 延迟任务(delay)
    • 并发控制(concurrency)
  • 如何把 traceId 贯穿到异步任务,做到可追踪


🧠 一、为什么要队列?

典型下单流程往往包含很多"非核心动作":

  • 发短信/邮件
  • 写埋点日志
  • 通知第三方系统
  • 生成优惠券、积分等

如果全部同步做,接口耗时会暴涨,且第三方慢/挂会拖死主业务。

正确姿势:

核心逻辑(扣库存、写订单)同步完成

非核心逻辑(通知、日志、延迟取消)丢队列异步做


🧱 二、准备环境与依赖

1)Redis(本地或 Docker)

Docker 启动 Redis:

bash 复制代码
docker run --name redis-nest -p 6379:6379 -d redis:7

2)安装依赖

bash 复制代码
npm install @nestjs/bull bull ioredis

Bull 内部使用 Redis;前面教程已经用过 ioredis,因此体系一致。


🧩 三、模块结构(本章最小闭环可运行)

我们基于一个"下单示例"来做队列:

  • /orders/create/:productId:创建订单(同步)

  • 同步成功后写入队列:

    • 发送通知(重试)
    • 写操作日志(并发控制)
    • 延迟取消订单(delay)

目录结构建议:

复制代码
src/
├── app.module.ts
├── common/
│   └── context/
│       ├── request-context.ts
│       └── trace.util.ts
├── queue/
│   ├── queue.module.ts
│   ├── jobs/
│   │   ├── order.jobs.ts
│   │   └── order.processor.ts
│   └── queue.constants.ts
└── orders/
    ├── orders.module.ts
    ├── orders.service.ts
    └── orders.controller.ts

如果你跟着教程做,那你现在应该已经有 traceId(第13章),这里直接复用即可。


🔧 四、配置 BullModule(全局)

src/app.module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { QueueModule } from './queue/queue.module';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [
    // Bull 全局连接 Redis
    BullModule.forRoot({
      redis: { host: '127.0.0.1', port: 6379 },
      // Bull 默认参数也可以在这里统一配置
      defaultJobOptions: {
        removeOnComplete: true, // 生产建议开启,避免 Redis 堆积
        removeOnFail: false,    // 失败任务保留用于排查(或配置保留数量)
      },
    }),
    QueueModule,
    OrdersModule,
  ],
})
export class AppModule {}

🧱 五、QueueModule:注册队列 + 注册 Processor

1)常量定义

src/queue/queue.constants.ts

ts 复制代码
export const QUEUE_ORDER = 'orderQueue';

export const JOB_SEND_NOTIFICATION = 'sendNotification';
export const JOB_WRITE_AUDIT_LOG = 'writeAuditLog';
export const JOB_AUTO_CANCEL_ORDER = 'autoCancelOrder';

2)QueueModule

src/queue/queue.module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { QUEUE_ORDER } from './queue.constants';
import { OrderJobs } from './jobs/order.jobs';
import { OrderProcessor } from './jobs/order.processor';

@Module({
  imports: [
    BullModule.registerQueue({
      name: QUEUE_ORDER,
    }),
  ],
  providers: [OrderJobs, OrderProcessor],
  exports: [OrderJobs], // 给 OrdersService 调用(Producer)
})
export class QueueModule {}

🧑‍🍳 六、Producer:封装任务投递(OrderJobs)

这样做的好处:业务层不用关心 Bull 细节,代码更干净。

src/queue/jobs/order.jobs.ts

ts 复制代码
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
import {
  QUEUE_ORDER,
  JOB_SEND_NOTIFICATION,
  JOB_WRITE_AUDIT_LOG,
  JOB_AUTO_CANCEL_ORDER,
} from '../queue.constants';

@Injectable()
export class OrderJobs {
  constructor(@InjectQueue(QUEUE_ORDER) private readonly queue: Queue) {}

  /**
   * 发送通知:失败自动重试 + 指数退避
   */
  async sendNotification(payload: {
    orderId: string;
    userId: number;
    traceId?: string;
  }) {
    return this.queue.add(JOB_SEND_NOTIFICATION, payload, {
      attempts: 3,
      backoff: { type: 'exponential', delay: 2000 },
    });
  }

  /**
   * 写审计日志:通常不需要重试太多,失败可落库/落文件
   */
  async writeAuditLog(payload: {
    action: string;
    orderId: string;
    traceId?: string;
  }) {
    return this.queue.add(JOB_WRITE_AUDIT_LOG, payload, {
      attempts: 2,
      backoff: { type: 'fixed', delay: 1000 },
    });
  }

  /**
   * 延迟任务:未支付订单自动取消
   */
  async autoCancelOrder(payload: { orderId: string; traceId?: string }) {
    return this.queue.add(JOB_AUTO_CANCEL_ORDER, payload, {
      delay: 30 * 60 * 1000, // 30分钟后执行
      attempts: 1,
    });
  }
}

🧠 七、Consumer:Processor 处理任务(OrderProcessor)

src/queue/jobs/order.processor.ts

ts 复制代码
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import {
  QUEUE_ORDER,
  JOB_SEND_NOTIFICATION,
  JOB_WRITE_AUDIT_LOG,
  JOB_AUTO_CANCEL_ORDER,
} from '../queue.constants';

@Processor(QUEUE_ORDER)
export class OrderProcessor {
  /**
   * 发送通知(并发 5)
   * 适合:发送邮件/短信/站内信等第三方调用
   */
  @Process({ name: JOB_SEND_NOTIFICATION, concurrency: 5 })
  async handleSendNotification(job: Job) {
    const { orderId, userId, traceId } = job.data;

    // 生产建议:这里用你自己的 Logger(带 traceId)
    console.log(`[sendNotification] traceId=${traceId} orderId=${orderId} userId=${userId}`);

    // 模拟第三方慢调用
    await new Promise((r) => setTimeout(r, 800));

    // 模拟偶发失败(用于验证重试机制)
    if (Math.random() < 0.1) {
      throw new Error('第三方短信服务失败(模拟)');
    }

    return { ok: true };
  }

  /**
   * 写审计日志(并发 10)
   * 适合:埋点、行为记录、关键操作审计
   */
  @Process({ name: JOB_WRITE_AUDIT_LOG, concurrency: 10 })
  async handleWriteAuditLog(job: Job) {
    const { action, orderId, traceId } = job.data;
    console.log(`[writeAuditLog] traceId=${traceId} action=${action} orderId=${orderId}`);

    // 模拟写入日志系统/数据库
    await new Promise((r) => setTimeout(r, 100));

    return { ok: true };
  }

  /**
   * 延迟取消订单(并发 3)
   * 适合:超时自动取消、自动确认收货、自动退款等
   */
  @Process({ name: JOB_AUTO_CANCEL_ORDER, concurrency: 3 })
  async handleAutoCancel(job: Job) {
    const { orderId, traceId } = job.data;
    console.log(`[autoCancelOrder] traceId=${traceId} orderId=${orderId}`);

    // 真实逻辑:查订单状态,如果未支付则取消并回滚库存
    await new Promise((r) => setTimeout(r, 200));

    return { canceled: true };
  }
}

🔗 八、Orders:在下单后投递队列(完整闭环)

OrdersModule

src/orders/orders.module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
import { QueueModule } from '../queue/queue.module';

@Module({
  imports: [QueueModule],
  providers: [OrdersService],
  controllers: [OrdersController],
})
export class OrdersModule {}

OrdersService:同步成功后投递任务

src/orders/orders.service.ts

ts 复制代码
import { Injectable } from '@nestjs/common';
import { OrderJobs } from '../queue/jobs/order.jobs';

// 如果你第13章用了 AsyncLocalStorage,这里可直接 getTraceId()
// 我这里做成可选,避免你没接入时跑不起来
function getTraceIdSafe(): string | undefined {
  return undefined;
}

@Injectable()
export class OrdersService {
  constructor(private readonly orderJobs: OrderJobs) {}

  async createOrder(productId: number) {
    // 1️⃣ 核心逻辑:扣库存 + 写订单(本章用模拟)
    // 真实项目:这里会调用第16章的库存扣减 + 事务写订单
    const orderId = `ORDER_${Date.now()}`;
    const userId = 1;

    const traceId = getTraceIdSafe();

    // 2️⃣ 非核心逻辑:投递队列(异步)
    await this.orderJobs.sendNotification({ orderId, userId, traceId });
    await this.orderJobs.writeAuditLog({ action: 'CREATE_ORDER', orderId, traceId });
    await this.orderJobs.autoCancelOrder({ orderId, traceId });

    // 3️⃣ 主接口快速返回
    return { orderId, productId, userId, queued: true };
  }
}

OrdersController

src/orders/orders.controller.ts

ts 复制代码
import { Controller, Post, Param, ParseIntPipe } from '@nestjs/common';
import { OrdersService } from './orders.service';

@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Post('create/:productId')
  create(@Param('productId', ParseIntPipe) productId: number) {
    return this.ordersService.createOrder(productId);
  }
}

🧪 九、测试方式

1)启动 Redis(如果没启动)

bash 复制代码
docker ps | grep redis-nest || docker run --name redis-nest -p 6379:6379 -d redis:7

2)启动 Nest

bash 复制代码
npm run start

3)请求下单接口

http 复制代码
POST http://localhost:3000/orders/create/1

你会看到:

  • 接口立刻返回 { queued: true }

  • 控制台异步输出:

    • sendNotification(可能重试)
    • writeAuditLog
    • autoCancelOrder(30 分钟后触发)

🧠 十、生产级注意事项(非常重要)

1)任务投递时机

务必保证:

数据库事务提交成功后 再 add job

否则会出现:

  • 队列执行了,但订单数据没落库(事务回滚)

2)失败任务处理策略

  • 重要任务(发货、扣款)必须可重试 + 告警
  • 非重要任务(埋点)失败可降级

3)队列拆分建议

不要把所有任务丢到一个队列里,推荐按职责拆:

  • orderQueue
  • notifyQueue
  • auditQueue

这样可控性更强,也更容易扩容。


✅ 本章小结

本篇你已经完成了一个"企业级队列闭环":

  • Bull + Redis 集成
  • Producer(OrderJobs)封装投递
  • Processor(OrderProcessor)消费执行
  • 自动重试 + 退避
  • 延迟任务
  • 并发控制
  • 与订单主流程解耦,实现削峰填谷
相关推荐
代码的小搬运工2 小时前
网络请求(NSURL、NSURLRequest、NSURLSessionDataTask、协议回调与 JSON 数据的基本流向)
网络·数据库·json
钛态8 小时前
前端安全防线:CSRF 攻击链路与双重 Token 校验的工程实现
前端·vue·react·web
Cx330❀8 小时前
【MySQL基础】一文吃透“表的约束”:从 Null/Default 到主外键的终极安全法则
linux·服务器·数据库·c++·mysql·安全
c238568 小时前
第二篇:《测试指挥官:可视化单题自测框架(含 assert 实操)》
java·数据库·c++·算法·安全性测试
我科绝伦(Huanhuan Zhou)8 小时前
MySQL极端场景数据丢失防护:从Crash-Safe到异地备份
数据库·mysql
辞旧 lekkk8 小时前
【Redis初阶】常见数据类型
开发语言·数据库·c++·redis·学习·缓存·bootstrap
吴声子夜歌8 小时前
Redis 3.x——集群节点通信
redis·缓存·集群
张拭心9 小时前
技术管理笔记:让我“燃尽”的一天
前端
明志数科9 小时前
人形机器人格斗场景下的技术挑战:动力学控制、感知融合与实时决策
网络·数据库
酉鬼女又兒9 小时前
零基础入门 DeepSeek V4 Pro API 开发:从环境搭建、消息格式规范到翻译函数实战、少样本提示、多轮对话聊天机器人与常见报错全流程详解指南
大数据·网络·数据库·人工智能·macos·机器人·github