PHP异步HTTP请求需重建句柄重试:curl_multi失败须curl_close后重新init;协程框架可用指数退避重试;超时参数要合理,封装函数应区分可重试错误(如超时、5xx)与不可重试错误(如4xx)。

PHP 本身没有原生的异步 HTTP 请求能力(curl_exec() 是同步阻塞的),所谓“PHP 异步 HTTP 请求”,通常指以下三种场景之一:
- 使用
curl_multi_*实现伪异步并发请求(多路复用,仍运行在单线程内) - 使用
ReactPHP、Amp等协程/事件驱动框架实现真正异步 - 调用外部工具(如
guzzlehttp/guzzle配合GuzzleHttp\Handler\CurlMultiHandler或Swoole\Http\Client)
但无论哪种,超时后的重试逻辑不能依赖“继续用同一个句柄重试”——因为句柄已失效或状态不可靠。重试必须重建请求上下文。
下面分场景说明怎么做重试机制:
明确哪些错误值得重试
不是所有失败都要重试,关键看错误根源:
立即学习“PHP免费学习笔记(深入)”;
- ✅ 可重试:
-
CURLE_OPERATION_TIMEDOUT(28)、CURLE_COULDNT_CONNECT(7)、CURLE_SEND_ERROR(55)等底层网络错误 - HTTP 状态码为
0(无响应)、502、503、504、5xx(服务端临时故障)
-
- ❌ 不该重试:
-
4xx错误(如400参数错、401未授权、404不存在)——问题在客户端或业务逻辑 -
curl_errno()返回0但响应体为空且状态码是200?需结合业务判断,一般不盲目重试
-
提示:每次失败务必调用
curl_error($ch)记录日志,否则你永远不知道第 2 次重试为啥又挂了。
使用 curl_multi 时的重试要点
curl_multi 适合并发发多个请求,但它不支持对单个失败请求“就地重试”:
- 一旦某个 handle 执行失败(
curl_multi_info_read()返回 error),必须:- 调用
curl_close($ch)彻底释放该句柄 - 用全新
curl_init()创建句柄,重新设置 URL、POST 数据、超时等全部选项 - 加入新的 multi 句柄(或单独用
curl_exec()重试)
- 调用
- 切忌把失败句柄留在 multi 中反复
curl_multi_exec()—— 它不会自动恢复
协程/事件驱动框架中的重试(如 Swoole 或 Amp)
这类环境支持真正的非阻塞 IO,重试更自然:
- 每次请求都是独立协程,失败后可直接
co::sleep(随机毫秒)再发起新请求 - 推荐用指数退避:第 1 次失败后等
500ms,第 2 次等1200ms,第 3 次等2800ms(带 ±20% 随机偏移,防雪崩) - 最多重试 3 次,超过则抛出异常或触发 onFailure 回调(如写告警、存失败队列)
超时参数必须合理设置,否则重试没意义
-
CURLOPT_CONNECTTIMEOUT_MS:建连阶段超时,设2000–4000毫秒(DNS 解析+TCP 握手) -
CURLOPT_TIMEOUT_MS:整次请求上限,设8000–12000毫秒(含传输) - ⚠️ 别设成
30000+ 重试 3 次 → 实际最长等 90 秒,调用方早超时了
封装一个可复用的重试函数(兼容普通 & multi 场景)
function postWithRetry(string $url, array $data, int $maxRetries = 3): ?array
{
$delayMs = 500;
for ($i = 0; $i <= $maxRetries; $i++) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 3000,
CURLOPT_TIMEOUT_MS => 10000,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errno = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
// 成功或明确不可重试,直接返回
if ($result !== false && $httpCode >= 200 && $httpCode < 400) {
return ['success' => true, 'body' => $result, 'code' => $httpCode];
}
if ($errno === 0 && $httpCode >= 400 && $httpCode < 500) {
return ['success' => false, 'reason' => 'client_error', 'code' => $httpCode];
}
if ($i === $maxRetries) {
return ['success' => false, 'reason' => 'max_retries_exhausted', 'error' => $error];
}
// 可重试:等一会再试
usleep($delayMs * 1000);
$delayMs = (int) min($delayMs * 2, 4000); // 指数增长,上限 4s
}
return null;
}不复杂但容易忽略



















