yii2 httpClient 不支持原生 multi 并发,因其未暴露 curl_multi_init 接口且请求生命周期为单次绑定;应改用 Guzzle,其原生支持 promise/Pool 并发、上下文隔离与完整中间件能力。

Yii2 HttpClient 不支持原生 multi 并发,别硬套 cURL multi
Yii2 的 yiihttpclientClient 底层封装的是 PHP stream 或 cURL,但**完全没暴露 curl_multi_init 接口**。试图在 send() 前手动塞一堆 request 进去、再调 multiExec,会直接报错或静默失败——因为它的 request 生命周期和执行模型是单次绑定的。
常见错误现象:Call to undefined method yiihttpclientClient::multiSend(),或者用自定义 wrapper 调了 curl_multi_add_handle 后,send() 返回 NULL / timeout / 乱序响应。
- 它设计目标是「链式构建 + 单次发送」,不是高并发 HTTP 客户端
- 所有 request 对象共享同一个
Client实例的配置(如 timeout、headers),但彼此不感知对方状态 - 强行 patch 底层 cURL handler 会导致重试逻辑、cookie jar、Event hook 全部失效
用 Guzzle 替代 HttpClient 是最稳的方案
Yii2 项目里混用 Guzzle 不仅合法,而且比折腾 HttpClient 更轻量、更可控。Guzzle 原生支持 Promise + Pool,能真正并行发请求,同时保留中间件、重试、日志等能力。
使用场景:需要 5+ 个外部 API 同时拉取(比如聚合用户资料、订单状态、库存),且不能接受串行等待。
- 安装:
composer require guzzlehttp/guzzle - 初始化 client 时复用 Yii2 的基础配置(如 timeout、base_uri):
$client = new GuzzleHttpClient(['timeout' => 5, 'base_uri' => 'https://api.example.com/']) - 并发控制用
Pool,避免无限制开连接:new GuzzleHttpPool($client, $requests, ['concurrency' => 10]) - 注意:不要在
foreach里直接$client->get(),那还是串行;必须用Promise构建异步队列
use GuzzleHttpPool; use GuzzleHttpClient; $client = new Client(['timeout' => 3]); $promises = array_map(function ($id) use ($client) { return $client->getAsync("/user/{$id}"); }, [1, 2, 3, 4, 5]); $results = Pool::batch($client, $promises, ['concurrency' => 3]);
如果非要用 HttpClient,只能模拟“伪并发”
本质是把多个 request 放进队列,用 PHP 的 pcntl_fork 或 amphp 模拟并行——但这已经脱离 HttpClient 范畴,且在 Web SAPI(如 FPM)下基本不可用。更现实的做法是「分批 + 异步任务」。
- 把 20 个请求拆成每批 4 个,用
foreach循环 send,每批之间加usleep(10000)避免打爆目标服务 - 把请求丢进 Yii2 的
yiiqueue(如 DB/redis 队列),由 worker 进程异步处理,靠队列调度实现资源隔离 - 别依赖
set_time_limit(0)硬扛超时,Web 请求生命周期有限,超时后连接会被 nginx/apache 中断 - HttpClient 的
timeout参数对 DNS 解析、TCP 握手、TLS 握手都生效,设太小容易误判失败
并发时 Cookie、Header、认证信息容易串
无论用哪种方案,并发请求共享同一份 client 实例时,CookieJar 和默认 headers 是全局状态。比如 A 请求设置了 Authorization: Bearer xxx,B 请求还没发完就覆盖了 Token,结果部分请求带错凭据。
- Guzzle 每个
Request可独立 set headers:$request = new Request('GET', '/user', ['headers' => ['Authorization' => 'Bearer yyy']]) - HttpClient 的每个
Request对象也支持单独 set:$request->addHeaders(['X-Trace-ID' => uniqid()]),但要注意不要在循环外复用$request实例 - 避免在 client 级别 set
cookies,改用 request 级别传Cookieheader 字符串 - JWT 场景下,token 过期时间短,务必每个请求都重新生成 Authorization header,别缓存 client 实例
实际跑通的关键不在“怎么发”,而在“怎么管住并发数、怎么隔离上下文、怎么兜住失败”。越想省事用一个 client 打到底,后面 debug 越像在找幽灵 bug。