本文目录导读:

在PHP项目中实现指数退避算法(Exponential Backoff)的重试间隔,通常用于处理网络请求、数据库连接失败等临时性错误,以下是几种常见实现方式:
基础指数退避实现
<?php
class ExponentialBackoff {
private $maxRetries;
private $baseDelay;
private $maxDelay;
public function __construct(
int $maxRetries = 3,
int $baseDelay = 100, // 毫秒
int $maxDelay = 30000 // 30秒
) {
$this->maxRetries = $maxRetries;
$this->baseDelay = $baseDelay;
$this->maxDelay = $maxDelay;
}
public function getDelay(int $attempt): int {
// 计算延迟:baseDelay * 2^attempt
$delay = $this->baseDelay * pow(2, $attempt);
// 添加随机抖动(jitter),避免惊群效应
$delay = mt_rand($delay, (int)($delay * 1.5));
// 限制最大延迟
return min($delay, $this->maxDelay);
}
public function execute(callable $operation) {
$lastException = null;
for ($attempt = 0; $attempt < $this->maxRetries; $attempt++) {
try {
return $operation();
} catch (\Throwable $e) {
$lastException = $e;
if ($attempt < $this->maxRetries - 1) {
$delayMs = $this->getDelay($attempt);
usleep($delayMs * 1000); // 转换为微秒
}
}
}
throw $lastException ?? new \RuntimeException('Operation failed');
}
}
// 使用示例
$backoff = new ExponentialBackoff(5, 200, 10000);
$result = $backoff->execute(function() {
// 模拟可能失败的操作
$response = file_get_contents('https://api.example.com/data');
return json_decode($response, true);
});
带完整功能的改良版
<?php
class SmartExponentialBackoff {
private int $maxRetries;
private int $baseDelay;
private int $maxDelay;
private bool $jitter;
private array $retryableExceptions = [];
private $onRetryCallback;
public function __construct(
int $maxRetries = 3,
int $baseDelayMs = 100,
int $maxDelayMs = 30000,
bool $jitter = true
) {
$this->maxRetries = max(1, $maxRetries);
$this->baseDelay = max(1, $baseDelayMs);
$this->maxDelay = max($this->baseDelay, $maxDelayMs);
$this->jitter = $jitter;
}
/**
* 设置可重试的异常类型
*/
public function setRetryableExceptions(array $exceptions): self {
$this->retryableExceptions = $exceptions;
return $this;
}
/**
* 设置重试回调
*/
public function onRetry(callable $callback): self {
$this->onRetryCallback = $callback;
return $this;
}
/**
* 计算延迟时间(毫秒)
*/
public function getDelayMs(int $attempt): int {
// 指数计算:baseDelay * 2^attempt
$delay = min(
$this->baseDelay * pow(2, $attempt),
$this->maxDelay
);
// 添加随机抖动
if ($this->jitter && $delay > 0) {
$delay = mt_rand(
(int)($delay * 0.5),
(int)($delay * 1.5)
);
}
return max(0, $delay);
}
/**
* 执行操作并自动重试
*/
public function execute(callable $operation) {
$lastException = null;
$attempt = 0;
while ($attempt < $this->maxRetries) {
try {
return $operation();
} catch (\Throwable $e) {
$lastException = $e;
// 检查是否应该重试
if (!$this->shouldRetry($e, $attempt)) {
throw $e;
}
$attempt++;
if ($attempt < $this->maxRetries) {
$delayMs = $this->getDelayMs($attempt - 1);
// 触发重试回调
if ($this->onRetryCallback) {
call_user_func($this->onRetryCallback, $attempt, $delayMs, $e);
}
// 等待延迟时间
usleep($delayMs * 1000);
}
}
}
throw $lastException;
}
/**
* 判断是否应该重试
*/
private function shouldRetry(\Throwable $e, int $attempt): bool {
if ($attempt >= $this->maxRetries) {
return false;
}
if (empty($this->retryableExceptions)) {
return true; // 默认所有异常都重试
}
foreach ($this->retryableExceptions as $exceptionClass) {
if ($e instanceof $exceptionClass) {
return true;
}
}
return false;
}
}
// 使用示例
$backoff = new SmartExponentialBackoff(
maxRetries: 5,
baseDelayMs: 200,
maxDelayMs: 15000,
jitter: true
)
->setRetryableExceptions([
\RuntimeException::class,
\GuzzleHttp\Exception\ServerException::class
])
->onRetry(function(int $attempt, int $delayMs, \Throwable $e) {
echo "重试 #{$attempt},等待 {$delayMs}ms,异常: " . $e->getMessage() . "\n";
});
try {
$result = $backoff->execute(function() {
// API调用
$client = new \GuzzleHttp\Client();
$response = $client->get('https://api.example.com/data', [
'timeout' => 5,
'connect_timeout' => 3
]);
return json_decode($response->getBody(), true);
});
} catch (\Throwable $e) {
echo "最终失败: " . $e->getMessage() . "\n";
}
轻量级函数实现
<?php
function retryWithExponentialBackoff(callable $operation, int $maxRetries = 3, int $baseDelayMs = 100) {
$attempt = 0;
while ($attempt < $maxRetries) {
try {
return $operation();
} catch (\Throwable $e) {
$attempt++;
if ($attempt >= $maxRetries) {
throw $e;
}
// 计算延迟:baseDelay * 2^(attempt-1) + random(0, 1000)
$delayMs = $baseDelayMs * pow(2, $attempt - 1) + mt_rand(0, 1000);
$delayMs = min($delayMs, 30000); // 上限30秒
usleep($delayMs * 1000);
}
}
}
// 使用
try {
$data = retryWithExponentialBackoff(function() {
return file_get_contents('https://api.example.com/data');
}, 5, 500);
} catch (\Throwable $e) {
echo "请求失败: " . $e->getMessage();
}
使用现有库(推荐)
// composer require guzzlehttp/retry-subscriber
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Client;
$handlerStack = HandlerStack::create();
// 添加重试中间件
$retryMiddleware = Middleware::retry(
function ($retries, $request, $response, $exception) {
// 最多重试3次
if ($retries >= 3) {
return false;
}
// 只在服务器错误时重试
if ($response && $response->getStatusCode() >= 500) {
return true;
}
return $exception instanceof \GuzzleHttp\Exception\ConnectException;
},
function ($retries) {
// 指数退避:100ms, 200ms, 400ms
return 100 * pow(2, $retries);
}
);
$handlerStack->push($retryMiddleware);
$client = new Client(['handler' => $handlerStack]);
最佳实践建议
- 总是添加随机抖动:避免多个客户端同时重试导致服务器过载
- 设置最大延迟上限:通常30秒-60秒
- 区分可重试错误:不要对所有错误都重试(如400 Bad Request不需要重试)
- 记录重试日志:便于调试和监控
- 考虑使用库:生产环境推荐使用成熟的库(如Guzzle的重试中间件)
选择哪种实现方式取决于你的具体需求:
- 简单场景使用函数实现
- 复杂场景使用面向对象版本
- HTTP请求使用Guzzle中间件