本文目录导读:

- 指数退避 + 随机抖动(最推荐)
- 使用 Redis / 内存计数器(防止进程内/分布式重试风暴)
- 使用 Guzzle 的重试中间件(HTTP 请求重试)
- 电路熔断器(Circuit Breaker)
- 防止重试风暴的最佳实践
“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) {
// 无延迟、无限制,瞬间打爆下游
}
}