PHP项目重试间隔如何采用指数退避算法

wen PHP项目 26

本文目录导读:

PHP项目重试间隔如何采用指数退避算法

  1. 基础指数退避实现
  2. 带完整功能的改良版
  3. 轻量级函数实现
  4. 使用现有库(推荐)
  5. 最佳实践建议

在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]);

最佳实践建议

  1. 总是添加随机抖动:避免多个客户端同时重试导致服务器过载
  2. 设置最大延迟上限:通常30秒-60秒
  3. 区分可重试错误:不要对所有错误都重试(如400 Bad Request不需要重试)
  4. 记录重试日志:便于调试和监控
  5. 考虑使用库:生产环境推荐使用成熟的库(如Guzzle的重试中间件)

选择哪种实现方式取决于你的具体需求:

  • 简单场景使用函数实现
  • 复杂场景使用面向对象版本
  • HTTP请求使用Guzzle中间件

抱歉,评论功能暂时关闭!