跳到主要内容

重试机制 retry

问题

实现一个带重试功能的异步请求函数,在请求失败时自动重试指定次数。

答案

重试机制是处理网络不稳定、服务临时不可用等场景的常用方案。


基础实现

async function retry<T>(
fn: () => Promise<T>,
maxRetries: number
): Promise<T> {
let lastError: Error | null = null;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
console.log(`重试第 ${attempt + 1} 次...`);
}
}
}

throw lastError;
}

// 使用
const fetchData = () =>
fetch('/api/data').then((res) => {
if (!res.ok) throw new Error('请求失败');
return res.json();
});

try {
const data = await retry(fetchData, 3);
console.log(data);
} catch (error) {
console.log('最终失败:', error);
}

带延迟的重试

interface RetryOptions {
maxRetries: number;
delay: number; // 重试间隔
exponential?: boolean; // 是否指数退避
}

async function retryWithDelay<T>(
fn: () => Promise<T>,
options: RetryOptions
): Promise<T> {
const { maxRetries, delay, exponential = false } = options;
let lastError: Error | null = null;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;

if (attempt < maxRetries) {
// 计算延迟时间
const waitTime = exponential
? delay * Math.pow(2, attempt) // 指数退避
: delay;

console.log(`等待 ${waitTime}ms 后重试...`);
await sleep(waitTime);
}
}
}

throw lastError;
}

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// 使用
const data = await retryWithDelay(fetchData, {
maxRetries: 3,
delay: 1000,
exponential: true, // 1s, 2s, 4s
});

完整版实现

interface FullRetryOptions {
maxRetries?: number;
delay?: number;
exponential?: boolean;
maxDelay?: number; // 最大延迟时间
timeout?: number; // 单次请求超时
shouldRetry?: (error: Error, attempt: number) => boolean;
onRetry?: (error: Error, attempt: number) => void;
abortSignal?: AbortSignal; // 支持取消
}

async function retryFull<T>(
fn: () => Promise<T>,
options: FullRetryOptions = {}
): Promise<T> {
const {
maxRetries = 3,
delay = 1000,
exponential = true,
maxDelay = 30000,
timeout,
shouldRetry = () => true,
onRetry,
abortSignal,
} = options;

let lastError: Error | null = null;
let attempt = 0;

while (attempt <= maxRetries) {
// 检查是否取消
if (abortSignal?.aborted) {
throw new Error('重试被取消');
}

try {
// 带超时的执行
const result = timeout
? await withTimeout(fn(), timeout)
: await fn();
return result;
} catch (error) {
lastError = error as Error;

// 判断是否应该重试
if (attempt >= maxRetries || !shouldRetry(lastError, attempt)) {
break;
}

// 回调
onRetry?.(lastError, attempt);

// 计算延迟
const waitTime = exponential
? Math.min(delay * Math.pow(2, attempt), maxDelay)
: delay;

await sleep(waitTime);
attempt++;
}
}

throw lastError;
}

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), ms)
),
]);
}

// 使用
const result = await retryFull(fetchData, {
maxRetries: 5,
delay: 500,
exponential: true,
maxDelay: 10000,
timeout: 5000,
shouldRetry: (error) => {
// 只重试网络错误,不重试 4xx 错误
return error.message.includes('network');
},
onRetry: (error, attempt) => {
console.log(`${attempt + 1} 次重试,错误: ${error.message}`);
},
});

带抖动的指数退避

function getDelayWithJitter(
baseDelay: number,
attempt: number,
maxDelay: number
): number {
// 指数退避
const exponentialDelay = baseDelay * Math.pow(2, attempt);
// 添加随机抖动 (0.5 ~ 1.5 倍)
const jitter = 0.5 + Math.random();
return Math.min(exponentialDelay * jitter, maxDelay);
}

async function retryWithJitter<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error | null = null;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;

if (attempt < maxRetries) {
const delay = getDelayWithJitter(baseDelay, attempt, 30000);
console.log(`等待 ${Math.round(delay)}ms 后重试`);
await sleep(delay);
}
}
}

throw lastError;
}

Promise 版本(不使用 async/await)

function retryPromise<T>(
fn: () => Promise<T>,
maxRetries: number,
delay = 0
): Promise<T> {
return new Promise((resolve, reject) => {
const attempt = (count: number): void => {
fn()
.then(resolve)
.catch((error) => {
if (count < maxRetries) {
setTimeout(() => attempt(count + 1), delay);
} else {
reject(error);
}
});
};

attempt(0);
});
}

递归实现

function retryRecursive<T>(
fn: () => Promise<T>,
maxRetries: number,
attempt = 0
): Promise<T> {
return fn().catch((error) => {
if (attempt >= maxRetries) {
return Promise.reject(error);
}
return retryRecursive(fn, maxRetries, attempt + 1);
});
}

装饰器实现

function RetryDecorator(maxRetries: number, delay = 0) {
return function (
target: unknown,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: unknown[]) {
let lastError: Error | null = null;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries && delay > 0) {
await sleep(delay);
}
}
}

throw lastError;
};

return descriptor;
};
}

// 使用
class ApiService {
@RetryDecorator(3, 1000)
async fetchData(): Promise<Response> {
return fetch('/api/data');
}
}

高阶函数版本

function withRetry<T extends (...args: unknown[]) => Promise<unknown>>(
fn: T,
maxRetries = 3,
delay = 1000
): T {
return (async (...args: Parameters<T>) => {
let lastError: Error | null = null;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(...args);
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
await sleep(delay * (attempt + 1));
}
}
}

throw lastError;
}) as T;
}

// 使用
const fetchWithRetry = withRetry(
(url: string) => fetch(url).then((r) => r.json()),
3,
1000
);

const data = await fetchWithRetry('/api/data');

常见面试问题

Q1: 什么情况下应该重试?

答案

应该重试不应该重试
网络超时400 Bad Request
502/503/504401 Unauthorized
连接被重置404 Not Found
DNS 解析失败业务逻辑错误
function shouldRetry(error: Error, statusCode?: number): boolean {
// 网络错误
if (error.message.includes('network') || error.message.includes('timeout')) {
return true;
}

// 可重试的 HTTP 状态码
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
if (statusCode && retryableStatusCodes.includes(statusCode)) {
return true;
}

return false;
}

Q2: 为什么需要指数退避和抖动?

答案

策略作用
指数退避避免持续高频请求压垮服务器
随机抖动避免多个客户端同时重试(雷暴效应)
// 没有抖动:多个客户端同时在 1s、2s、4s 重试
// 有抖动:客户端分散在不同时间重试
const jitter = 0.5 + Math.random(); // 0.5 ~ 1.5
const delay = baseDelay * Math.pow(2, attempt) * jitter;

Q3: 如何实现可取消的重试?

答案

function retryWithAbort<T>(
fn: () => Promise<T>,
signal: AbortSignal,
maxRetries = 3
): Promise<T> {
return new Promise((resolve, reject) => {
let attempt = 0;

const execute = async () => {
if (signal.aborted) {
reject(new Error('已取消'));
return;
}

try {
const result = await fn();
resolve(result);
} catch (error) {
if (attempt < maxRetries && !signal.aborted) {
attempt++;
setTimeout(execute, 1000);
} else {
reject(error);
}
}
};

signal.addEventListener('abort', () => {
reject(new Error('已取消'));
});

execute();
});
}

// 使用
const controller = new AbortController();
const promise = retryWithAbort(fetchData, controller.signal, 3);

// 需要时取消
controller.abort();

相关链接