20个例子掌握RxJS——第八章使用 retryWhen 实现失败重试机制

9 阅读5分钟

RxJS 实战:使用 retryWhen 实现失败重试机制

概述

在网络请求中,由于网络波动、服务器临时故障等原因,请求可能会失败。简单的重试机制(如 retry 操作符)可能不够灵活,无法满足复杂的需求。本章将介绍如何使用 retryWhen 操作符实现更灵活的重试机制,比如延迟重试、限制重试次数等。

retry 操作符的局限性

RxJS 提供了 retry 操作符,可以简单地重试指定次数:

this.http.get('/api/data').pipe(
  retry(3) // 失败后立即重试 3 次
)

问题

  • 立即重试,可能服务器还没恢复
  • 无法自定义重试逻辑(如延迟重试)
  • 无法根据错误类型决定是否重试

retryWhen 操作符简介

retryWhen 提供了更灵活的重试机制,它允许我们:

  1. 自定义重试逻辑:根据错误类型决定是否重试
  2. 延迟重试:在重试前等待一段时间
  3. 限制重试次数:使用 takescan 限制重试次数
  4. 指数退避:每次重试的延迟时间递增

基本语法

source$.pipe(
  retryWhen(errors => 
    errors.pipe(
      // 自定义重试逻辑
    )
  )
)

实战场景:延迟重试失败请求

假设我们有一个可能失败的 API,失败后需要等待 3 秒再重试,最多重试 3 次。

实现思路

  1. 使用 retryWhen 捕获错误
  2. 使用 scan 计数重试次数
  3. 使用 delay 延迟重试
  4. 使用 take 限制重试次数

核心代码

// 最大重试次数
private readonly maxRetries = 3;

// 发起请求(带重试逻辑)
private makeRequestWithRetry(): void {
  // 重置状态
  this.requestStatus = {
    status: 'requesting',
    retryCount: 0
  };
  this.cdr.detectChanges();
  
  this.http.get<ApiResponse>(`${this.apiBaseUrl}/api/fail`)
    .pipe(
      // 使用 retryWhen 实现失败后 3 秒重试
      retryWhen(errors => 
        errors.pipe(
          // 使用 scan 来计数重试次数
          scan((retryCount, error) => {
            // 更新重试次数
            this.requestStatus.retryCount = retryCount + 1;
            
            // 如果还没超过最大重试次数,更新状态为重试中
            if (retryCount < this.maxRetries) {
              this.requestStatus.status = 'retrying';
              this.cdr.detectChanges();
            }
            
            return retryCount + 1;
          }, 0),
          // 延迟 3 秒后重试
          delay(3000),
          // 最多重试 maxRetries 次
          take(this.maxRetries)
        )
      ),
      catchError(err => {
        // 如果最终失败,返回错误
        this.requestStatus.status = 'failed';
        this.requestStatus.error = err.message || '请求失败,已达到最大重试次数';
        this.cdr.detectChanges();
        return of(null);
      })
    )
    .subscribe({
      next: (response) => {
        if (response) {
          // 请求成功
          this.requestStatus.status = 'success';
          this.requestStatus.response = response;
          this.cdr.detectChanges();
        }
      },
      error: (err) => {
        // 处理错误(虽然已经在 catchError 中处理了)
        if (this.requestStatus.status !== 'failed') {
          this.requestStatus.status = 'failed';
          this.requestStatus.error = err.message || '请求失败';
          this.cdr.detectChanges();
        }
      }
    });
}

关键点解析

1. retryWhen 的工作机制

retryWhen 接收一个函数,该函数接收一个 Observable(错误流),返回一个 Observable。当返回的 Observable 发出值时,会重试源 Observable。

2. scan 操作符计数

scan 操作符用于累积值,这里用来计数重试次数:

scan((retryCount, error) => {
  return retryCount + 1; // 每次错误时递增
}, 0) // 初始值为 0

3. delay 延迟重试

delay(3000) 会在重试前等待 3 秒,给服务器恢复的时间。

4. take 限制重试次数

take(this.maxRetries) 确保最多重试 3 次,超过后不再重试。

5. 执行流程

  1. 第一次请求:失败 → 进入 retryWhen
  2. 第一次重试:等待 3 秒 → 重试 → 如果失败,继续
  3. 第二次重试:等待 3 秒 → 重试 → 如果失败,继续
  4. 第三次重试:等待 3 秒 → 重试 → 如果失败,不再重试
  5. 最终结果:成功或失败

高级用法

1. 指数退避(Exponential Backoff)

每次重试的延迟时间递增:

retryWhen(errors =>
  errors.pipe(
    scan((retryCount, error) => {
      const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // 指数递增,最多 10 秒
      return { retryCount: retryCount + 1, delay };
    }, { retryCount: 0, delay: 1000 }),
    mergeMap(({ delay }) => timer(delay)), // 使用动态延迟
    take(5)
  )
)

2. 根据错误类型决定是否重试

只对特定错误重试:

retryWhen(errors =>
  errors.pipe(
    mergeMap((error, index) => {
      // 只对 500 错误重试
      if (error.status === 500 && index < 3) {
        return timer(3000); // 延迟 3 秒后重试
      }
      return throwError(() => error); // 其他错误不重试
    })
  )
)

3. 重试前执行操作

在重试前执行一些操作(如刷新 Token):

retryWhen(errors =>
  errors.pipe(
    mergeMap((error, index) => {
      if (error.status === 401 && index < 1) {
        // 刷新 Token 后再重试
        return this.refreshToken().pipe(
          switchMap(() => timer(1000)) // 延迟 1 秒后重试
        );
      }
      return throwError(() => error);
    })
  )
)

与其他方案的对比

方案 1:使用 retry(简单但不灵活)

// ⚠️ 立即重试,无法延迟
this.http.get('/api/data').pipe(
  retry(3)
)

方案 2:手动实现(复杂)

// ⚠️ 需要手动管理状态和循环
let retryCount = 0;
const maxRetries = 3;

function makeRequest() {
  return this.http.get('/api/data').pipe(
    catchError(error => {
      if (retryCount < maxRetries) {
        retryCount++;
        return timer(3000).pipe(
          switchMap(() => makeRequest())
        );
      }
      return throwError(() => error);
    })
  );
}

方案 3:使用 retryWhen(推荐)✅

// ✅ 简洁、灵活
this.http.get('/api/data').pipe(
  retryWhen(errors =>
    errors.pipe(
      scan((count) => count + 1, 0),
      delay(3000),
      take(3)
    )
  )
)

实际应用场景

1. API 请求重试

// 网络请求失败后重试
this.http.get('/api/data').pipe(
  retryWhen(errors =>
    errors.pipe(
      scan((count) => count + 1, 0),
      mergeMap(count => {
        if (count > 3) {
          return throwError(() => new Error('重试次数过多'));
        }
        return timer(1000 * count); // 延迟时间递增
      })
    )
  )
)

2. WebSocket 连接重试

// WebSocket 连接失败后重试
connectWebSocket().pipe(
  retryWhen(errors =>
    errors.pipe(
      scan((count) => count + 1, 0),
      mergeMap(count => {
        if (count > 5) {
          return throwError(() => new Error('连接失败'));
        }
        return timer(2000 * count); // 延迟时间递增
      })
    )
  )
)

3. 文件上传重试

// 文件上传失败后重试
uploadFile(file).pipe(
  retryWhen(errors =>
    errors.pipe(
      scan((count) => count + 1, 0),
      mergeMap(count => {
        if (count > 2) {
          return throwError(() => new Error('上传失败'));
        }
        return timer(3000); // 固定延迟 3 秒
      })
    )
  )
)

性能优化建议

1. 合理设置重试次数

根据业务需求设置合理的重试次数:

  • 关键操作:3-5 次
  • 非关键操作:1-2 次
  • 实时性要求高:1 次或不重试

2. 使用指数退避

对于可能长时间故障的服务,使用指数退避:

retryWhen(errors =>
  errors.pipe(
    scan((count) => count + 1, 0),
    mergeMap(count => {
      const delay = Math.min(1000 * Math.pow(2, count), 30000);
      return timer(delay);
    }),
    take(5)
  )
)

3. 根据错误类型决定是否重试

只对可恢复的错误重试:

retryWhen(errors =>
  errors.pipe(
    mergeMap((error, index) => {
      // 只对网络错误和 5xx 错误重试
      if ((error.status >= 500 || !error.status) && index < 3) {
        return timer(3000);
      }
      return throwError(() => error);
    })
  )
)

注意事项

  1. 无限重试:确保使用 take 限制重试次数,避免无限重试
  2. 资源占用:重试会占用资源,需要合理设置重试次数和延迟
  3. 用户体验:给用户适当的反馈,告知正在重试
  4. 错误处理:确保最终失败时有适当的错误处理

总结

retryWhen 是实现灵活重试机制的强大工具,它允许我们:

  • 自定义重试逻辑:根据错误类型和次数决定是否重试
  • 延迟重试:在重试前等待,给服务器恢复的时间
  • 限制重试次数:避免无限重试
  • 指数退避:延迟时间递增,减少服务器压力

在实际项目中,根据具体需求选择合适的重试策略,既能提高请求的成功率,又能避免过度重试带来的资源浪费。

记住:重试是一种容错机制,但不是万能的。对于关键操作,还需要考虑其他容错方案,如降级、缓存等

码云地址:gitee.com/leeyamaster…