4. 集成熔断器:Bull + ioredis 与自定义装饰器

熔断器(Circuit Breaker)是微服务架构中防止级联故障的关键模式。当下游服务持续失败时,熔断器会快速拒绝请求,避免调用方线程阻塞和资源耗尽,给下游恢复时间。本章将基于 @nestjs/bull(任务队列)与 ioredis 实现基础熔断,并进一步封装为自定义装饰器,让熔断逻辑与业务代码解耦。

4.1 熔断器核心状态机

熔断器维护三个状态:CLOSED(关闭)、OPEN(打开)、HALF_OPEN(半开)。状态转换由失败计数、阈值和超时时间驱动。

关键指标: 熔断器在 CLOSED 状态下累计失败次数;超过阈值则切换到 OPEN;经过超时时间后进入 HALF_OPEN,允许少量探测请求;若探测成功则恢复 CLOSED,否则回到 OPEN。
熔断器状态机 CLOSED OPEN HALF_OPEN 失败数 ≥ 阈值 超时时间到 探测成功 探测失败

4.2 基于 @nestjs/bull 与 ioredis 的基础熔断实现

我们使用 @nestjs/bull 管理任务队列,ioredis 存储熔断状态(失败计数、状态、时间戳)。Redis 的原子操作和过期机制天然适合熔断器计数与超时。

4.2.1 安装依赖

npm install @nestjs/bull bull ioredis
npm install --save-dev @types/ioredis

4.2.2 创建熔断器服务

import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class CircuitBreakerService {
  private readonly redis: Redis;
  private readonly failureThreshold = 5;       // 失败阈值
  private readonly timeoutSeconds = 30;        // 熔断超时(秒)
  private readonly halfOpenMaxRequests = 3;    // 半开最大探测数

  constructor() {
    this.redis = new Redis({ host: 'localhost', port: 6379 });
  }

  // 获取当前熔断状态
  async getState(serviceKey: string): Promise<string> {
    const state = await this.redis.get(`cb:${serviceKey}:state`);
    return state || 'CLOSED';
  }

  // 记录失败并判断是否触发熔断
  async recordFailure(serviceKey: string): Promise<void> {
    const current = await this.redis.incr(`cb:${serviceKey}:failures`);
    await this.redis.expire(`cb:${serviceKey}:failures`, this.timeoutSeconds);
    if (current >= this.failureThreshold) {
      await this.redis.set(`cb:${serviceKey}:state`, 'OPEN');
      await this.redis.expire(`cb:${serviceKey}:state`, this.timeoutSeconds);
    }
  }

  // 记录成功(重置计数)
  async recordSuccess(serviceKey: string): Promise<void> {
    await this.redis.del(`cb:${serviceKey}:failures`);
    await this.redis.set(`cb:${serviceKey}:state`, 'CLOSED');
  }

  // 检查是否允许请求通过
  async allowRequest(serviceKey: string): Promise<boolean> {
    const state = await this.getState(serviceKey);
    if (state === 'CLOSED') return true;
    if (state === 'OPEN') {
      // 检查是否超时,超时则进入 HALF_OPEN
      const ttl = await this.redis.ttl(`cb:${serviceKey}:state`);
      if (ttl <= 0) {
        await this.redis.set(`cb:${serviceKey}:state`, 'HALF_OPEN');
        return true;
      }
      return false;
    }
    // HALF_OPEN:限制探测请求数
    const count = await this.redis.incr(`cb:${serviceKey}:halfopen_count`);
    await this.redis.expire(`cb:${serviceKey}:halfopen_count`, 10);
    return count <= this.halfOpenMaxRequests;
  }
}

4.2.3 在 Bull 处理器中集成熔断

import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { CircuitBreakerService } from './circuit-breaker.service';

@Processor('payment')
export class PaymentProcessor {
  constructor(private readonly cbService: CircuitBreakerService) {}

  @Process('charge')
  async handleCharge(job: Job) {
    const serviceKey = 'payment:charge';
    const allowed = await this.cbService.allowRequest(serviceKey);
    if (!allowed) {
      throw new Error('CircuitBreaker: request blocked');
    }
    try {
      // 模拟下游调用
      await this.callDownstream(job.data);
      await this.cbService.recordSuccess(serviceKey);
    } catch (err) {
      await this.cbService.recordFailure(serviceKey);
      throw err;
    }
  }

  private async callDownstream(data: any): Promise<void> {
    // 实际 HTTP/RPC 调用
  }
}

4.3 自定义装饰器实现熔断逻辑

为了消除重复的熔断检查代码,我们封装一个 @CircuitBreaker 装饰器,自动在方法执行前后进行状态判断和计数。

4.3.1 装饰器定义

import { SetMetadata } from '@nestjs/common';

export const CIRCUIT_BREAKER_KEY = 'circuit_breaker';

export interface CircuitBreakerOptions {
  serviceKey: string;
  failureThreshold?: number;
  timeoutSeconds?: number;
}

export const CircuitBreaker = (options: CircuitBreakerOptions) =>
  SetMetadata(CIRCUIT_BREAKER_KEY, options);

4.3.2 装饰器执行拦截器

import {
  Injectable,
  ExecutionContext,
  CallHandler,
  NestInterceptor,
  Inject,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, from, throwError } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { CircuitBreakerService } from './circuit-breaker.service';
import { CIRCUIT_BREAKER_KEY, CircuitBreakerOptions } from './circuit-breaker.decorator';

@Injectable()
export class CircuitBreakerInterceptor implements NestInterceptor {
  constructor(
    private readonly reflector: Reflector,
    private readonly cbService: CircuitBreakerService,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const options = this.reflector.get<CircuitBreakerOptions>(
      CIRCUIT_BREAKER_KEY,
      context.getHandler(),
    );
    if (!options) {
      return next.handle();
    }
    const { serviceKey } = options;

    return from(this.cbService.allowRequest(serviceKey)).pipe(
      switchMap((allowed) => {
        if (!allowed) {
          return throwError(() => new Error(`CircuitBreaker: ${serviceKey} is OPEN`));
        }
        return next.handle().pipe(
          catchError(async (err) => {
            await this.cbService.recordFailure(serviceKey);
            throw err;
          }),
          switchMap(async (result) => {
            await this.cbService.recordSuccess(serviceKey);
            return result;
          }),
        );
      }),
    );
  }
}

4.3.3 在 Controller 中使用

import { Controller, Post, Body, UseInterceptors } from '@nestjs/common';
import { CircuitBreaker } from './circuit-breaker.decorator';
import { CircuitBreakerInterceptor } from './circuit-breaker.interceptor';

@Controller('orders')
@UseInterceptors(CircuitBreakerInterceptor)
export class OrderController {
  @Post()
  @CircuitBreaker({ serviceKey: 'order:create', failureThreshold: 3, timeoutSeconds: 20 })
  async createOrder(@Body() dto: any) {
    // 业务逻辑,熔断由装饰器自动处理
    return { success: true };
  }
}

4.4 方案对比

维度 基础 Bull + ioredis 实现 自定义装饰器实现
代码侵入性 高:每个方法需手动调用 allowRequest / recordFailure 低:一行装饰器即可,业务代码无感知
可复用性 中等:需复制熔断逻辑到每个处理器 高:全局注册拦截器,装饰器可随处使用
灵活性 高:可针对不同任务定制阈值和超时 高:装饰器参数支持每个方法独立配置
测试难度 中等:需 mock Redis 和状态 低:拦截器可单独测试,装饰器元数据易模拟
适用场景 快速原型、简单任务队列 大型项目、统一治理、AOP 风格

4.5 避坑指南

注意:
  • Redis 连接泄漏: 每次请求都创建新 Redis 实例会导致连接数暴涨。应将 Redis 实例设为单例或使用连接池。
  • 原子性: incr 和 expire 不是原子操作,高并发下可能丢失过期时间。建议使用 Lua 脚本或 Redis 事务保证原子性。
  • 半开状态并发: 多个请求同时进入 HALF_OPEN 可能超过探测限制。使用 Redis 的 SETNX 或 Lua 脚本实现分布式锁。
  • 装饰器执行顺序: 如果同时使用多个拦截器,确保 CircuitBreakerInterceptor 在业务拦截器之前执行,否则熔断可能失效。

4.6 架构总览

下图展示了熔断器在 NestJS 微服务中的完整集成位置:

NestJS 熔断器集成架构 客户端请求 Controller @CircuitBreaker Bull 队列 Redis 熔断状态 下游服务
最佳实践: 生产环境中建议将熔断器与 @nestjs/bull 的失败重试策略结合使用。当熔断器 OPEN 时,Bull 的重试机制应暂停或延长间隔,避免无效重试加剧系统压力。