PHP 怎么PHP 重试风暴

wen PHP项目 1

本文目录导读:

PHP 怎么PHP 重试风暴

  1. 指数退避 + 随机抖动(最推荐)
  2. 使用 Redis / 内存计数器(防止进程内/分布式重试风暴)
  3. 使用 Guzzle 的重试中间件(HTTP 请求重试)
  4. 电路熔断器(Circuit Breaker)
  5. 防止重试风暴的最佳实践

“PHP 重试风暴”通常指的是在代码中因循环重试逻辑设计不当,导致短时间内发起海量重复请求,从而压垮下游服务(如数据库、API、Redis)的现象。

要避免或解决这个问题,核心策略是:限制重试次数、增加退避延迟、控制并发、设置熔断

以下是 PHP 中应对“重试风暴”的几种常见方案及代码示例:

指数退避 + 随机抖动(最推荐)

不要每次都立即重试,而是让等待时间逐渐变长,并加入随机值防止“惊群效应”。

<?php
function executeWithRetry(callable $operation, int $maxRetries = 3): mixed
{
    $attempt = 0;
    $baseDelay = 100; // 基础延迟 100 毫秒
    while (true) {
        try {
            return $operation();
        } catch (\Throwable $e) {
            $attempt++;
            if ($attempt > $maxRetries) {
                throw new \RuntimeException("重试 $maxRetries 次后失败: " . $e->getMessage(), 0, $e);
            }
            // 指数退避: 100ms, 200ms, 400ms
            $delay = $baseDelay * pow(2, $attempt - 1);
            // 随机抖动:加上0到50毫秒的随机值,防止多个请求同时重试
            $jitter = random_int(0, (int)($baseDelay * 0.5));
            $sleepMs = $delay + $jitter;
            usleep($sleepMs * 1000); // usleep 单位是微秒
        }
    }
}
// 使用示例
executeWithRetry(function() {
    // 你的数据库查询或第三方API调用
    return someUnreliableService();
});

使用 Redis / 内存计数器(防止进程内/分布式重试风暴)

如果多个进程或服务器同时重试,可以用 Redis 实现全局速率限制。

<?php
function safeRetryWithRateLimit(callable $operation, string $keyPrefix = 'retry:', string $cacheKey = null): mixed
{
    $maxRetries = 3;
    $attempt = 0;
    // 使用 Redis(需要引入 Predis 或 PhpRedis)
    $redis = new \Redis();
    $redis->connect('127.0.0.1', 6379);
    while (true) {
        try {
            return $operation();
        } catch (\Throwable $e) {
            $attempt++;
            if ($attempt > $maxRetries) {
                throw $e;
            }
            // 使用唯一标识(如请求ID或用户ID)限制频率
            $key = $keyPrefix . ($cacheKey ?: 'global');
            // 1秒内允许重试3次
            $current = $redis->incr($key);
            if ($current === 1) {
                $redis->expire($key, 1); // 设置1秒过期
            }
            if ($current > $maxRetries) {
                throw new \RuntimeException("超出重试速率限制");
            }
            usleep(200000); // 固定等待200ms
        }
    }
}

使用 Guzzle 的重试中间件(HTTP 请求重试)

如果你的重试是针对外部 HTTP API,可以使用 Guzzle 的 RetryMiddleware,它内置了指数退避。

<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
$handlerStack = HandlerStack::create();
// 延迟策略:重试次数越多,等待时间越长
$retryMiddleware = Middleware::retry(function (int $retries, Request $request, ?Response $response = null) {
    // 最多重试3次,且仅在服务器错误(5xx)时重试
    return $retries < 3 && ($response && $response->getStatusCode() >= 500);
}, function (int $numberOfRetries) {
    // 指数退避: 1s, 2s, 4s
    return 1000 * pow(2, $numberOfRetries);
});
$handlerStack->push($retryMiddleware);
$client = new Client(['handler' => $handlerStack]);
// 使用
$response = $client->get('https://example.com/unstable-api');

电路熔断器(Circuit Breaker)

当重试失败次数达到阈值后,直接不再重试(熔断),而不是不断尝试。

<?php
// 简化版:可以使用开源库如 "ackintosh/ganesha" 或自己实现
class CircuitBreaker
{
    private int $failureCount = 0;
    private int $threshold = 5;
    private bool $open = false;
    private ?int $lastFailureTime = null;
    private int $cooldownSeconds = 30;
    public function execute(callable $operation): mixed
    {
        if ($this->open) {
            if (time() - $this->lastFailureTime > $this->cooldownSeconds) {
                // 尝试半开
                $this->open = false;
                $this->failureCount = 0;
            } else {
                throw new \RuntimeException("熔断开启,请稍后重试");
            }
        }
        try {
            $result = $operation();
            $this->reset();
            return $result;
        } catch (\Throwable $e) {
            $this->failureCount++;
            $this->lastFailureTime = time();
            if ($this->failureCount >= $this->threshold) {
                $this->open = true;
            }
            throw $e;
        }
    }
    private function reset(): void
    {
        $this->failureCount = 0;
        $this->open = false;
    }
}

防止重试风暴的最佳实践

策略 解决的问题 适用场景
指数退避 + 抖动 立即重试导致并发洪峰 任何重试逻辑(基础推荐)
速率限制 多个进程/服务同时重试 分布式系统、API 调用
熔断器 持续重试无意义时 下游服务已崩溃
有限重试次数 无限重试压垮系统 一切重试逻辑的基础
手动观察降级 系统过载 生产环境必须配合

最核心的点:永远不要写类似下面这种“裸重试”代码,因为它是风暴的导火索:

// ❌ 绝对不要这样:
while (true) {
    try { /* 操作 */ } catch (\Exception $e) {
        // 无延迟、无限制,瞬间打爆下游
    }
}

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