RxJS 实战:使用 retryWhen 实现失败重试机制
概述
在网络请求中,由于网络波动、服务器临时故障等原因,请求可能会失败。简单的重试机制(如 retry 操作符)可能不够灵活,无法满足复杂的需求。本章将介绍如何使用 retryWhen 操作符实现更灵活的重试机制,比如延迟重试、限制重试次数等。
retry 操作符的局限性
RxJS 提供了 retry 操作符,可以简单地重试指定次数:
this.http.get('/api/data').pipe(
retry(3) // 失败后立即重试 3 次
)
问题:
- 立即重试,可能服务器还没恢复
- 无法自定义重试逻辑(如延迟重试)
- 无法根据错误类型决定是否重试
retryWhen 操作符简介
retryWhen 提供了更灵活的重试机制,它允许我们:
- 自定义重试逻辑:根据错误类型决定是否重试
- 延迟重试:在重试前等待一段时间
- 限制重试次数:使用
take或scan限制重试次数 - 指数退避:每次重试的延迟时间递增
基本语法
source$.pipe(
retryWhen(errors =>
errors.pipe(
// 自定义重试逻辑
)
)
)
实战场景:延迟重试失败请求
假设我们有一个可能失败的 API,失败后需要等待 3 秒再重试,最多重试 3 次。
实现思路
- 使用
retryWhen捕获错误 - 使用
scan计数重试次数 - 使用
delay延迟重试 - 使用
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. 执行流程
- 第一次请求:失败 → 进入
retryWhen - 第一次重试:等待 3 秒 → 重试 → 如果失败,继续
- 第二次重试:等待 3 秒 → 重试 → 如果失败,继续
- 第三次重试:等待 3 秒 → 重试 → 如果失败,不再重试
- 最终结果:成功或失败
高级用法
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);
})
)
)
注意事项
- 无限重试:确保使用
take限制重试次数,避免无限重试 - 资源占用:重试会占用资源,需要合理设置重试次数和延迟
- 用户体验:给用户适当的反馈,告知正在重试
- 错误处理:确保最终失败时有适当的错误处理
总结
retryWhen 是实现灵活重试机制的强大工具,它允许我们:
- 自定义重试逻辑:根据错误类型和次数决定是否重试
- 延迟重试:在重试前等待,给服务器恢复的时间
- 限制重试次数:避免无限重试
- 指数退避:延迟时间递增,减少服务器压力
在实际项目中,根据具体需求选择合适的重试策略,既能提高请求的成功率,又能避免过度重试带来的资源浪费。
记住:重试是一种容错机制,但不是万能的。对于关键操作,还需要考虑其他容错方案,如降级、缓存等。