请求重试与超时策略
场景
网络不稳定或服务端偶尔抖动时,如何设计合理的重试和超时机制,既保证成功率又避免雪崩?
方案设计
指数退避重试
retry-with-backoff.ts
interface RetryOptions {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
timeout?: number;
retryOn?: (error: unknown, attempt: number) => boolean;
}
async function fetchWithRetry<T>(
url: string,
init?: RequestInit,
options: RetryOptions = {}
): Promise<T> {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000,
timeout = 5000,
retryOn = () => true,
} = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// 超时控制
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...init,
signal: controller.signal,
});
clearTimeout(timer);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries || !retryOn(error, attempt)) {
throw error;
}
// 指数退避 + 随机抖动
const delay = Math.min(
baseDelay * 2 ** attempt + Math.random() * 1000,
maxDelay
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
幂等性判断
重要
只有幂等请求才应该重试:
- ✅ GET、HEAD、OPTIONS — 天然幂等
- ✅ PUT、DELETE — 通常幂等
- ❌ POST — 默认非幂等,重试可能导致重复提交
对于 POST 请求,需要服务端支持幂等 key 才能安全重试:
idempotent-retry.ts
async function safeRetryPost<T>(url: string, body: unknown): Promise<T> {
const idempotencyKey = crypto.randomUUID();
return fetchWithRetry(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
});
}
重试策略对比
| 策略 | 退避方式 | 适用场景 |
|---|---|---|
| 固定间隔 | 1s, 1s, 1s | 简单场景 |
| 线性退避 | 1s, 2s, 3s | 一般场景 |
| 指数退避 | 1s, 2s, 4s | 服务端压力大 |
| 指数退避+抖动 | 1s±rand, 2s±rand, 4s±rand | 推荐,避免惊群 |
常见面试问题
Q1: 指数退避为什么要加随机抖动(jitter)?
答案:
如果 1000 个客户端同时失败,不加抖动的话它们会在完全相同的时间点重试,造成惊群效应(thundering herd),再次打垮服务端。加入随机抖动可以打散重试请求的时间分布。
Q2: 接口超时应该设多少?
答案:
- 用户交互类(搜索、提交):3-5 秒
- 后台任务类(上传、导出):30-60 秒
- SSR 数据请求:1-3 秒(影响 TTFB)
建议根据接口 P99 延迟的 2-3 倍来设置超时时间,并配合 Loading 状态展示。