Hyperf单例对象成员变量被多请求共用,因Hyperf默认将服务注册为单例,整个Worker进程常驻内存,协程间共享同一实例;而协程上下文隔离不自动重置对象属性,导致如$this->user_id等成员变量被后续协程误读。

Hyperf单例对象的成员变量为什么会被多个请求共用
因为Hyperf默认把绝大多数服务(包括Controller、Service、MiddleWare)注册为单例(Singleton),整个Worker进程生命周期内只创建一次实例。协程之间共享这个对象,但协程上下文是隔离的——成员变量却不是。
这就像办公室里只有一台公用打印机,每个员工(协程)都用它打印自己的文件,但如果有人忘了清空上次打印的缓存设置(比如纸张尺寸、双面开关),下一个人按默认就直接沿用了——$this->user_id = 123 写进去,下一个协程读出来就是123,哪怕它本该是456。
- 单例对象在Worker启动时初始化,常驻内存,不随请求销毁
- 协程切换不重置对象属性,
protected $data是进程级共享的 - PHP本身没有协程感知的“自动属性隔离”机制
哪些写法最容易导致数据串号
以下代码看似合理,实则危险,尤其在压测或并发稍高时立刻暴露:
class UserService
{
protected array $cache = [];
<pre class="brush:php;toolbar:false;">public function getUser(int $id): array
{
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->db->fetchOne('SELECT * FROM user WHERE id = ?', [$id]);
}
return $this->cache[$id];
}}
问题不止在缓存,任何写入成员变量的操作都可能污染其他协程:
$this->requestId = $request->getAttribute('request_id')$this->currentUser = $user-
$this->errors[] = 'xxx'(数组追加,永不清理) - 甚至
static $counter++—— 静态变量在协程间完全共享
怎么安全地存请求级状态
必须把“属于当前请求”的数据,绑定到当前协程上下文,而不是对象实例上。
本页面提供企业级 PHP 协程框架 Hyperf 3.1.66 版本的官方源码下载与完整更新日志。重点解析 v3.1.66 版本中新增的 gRPC 多客户端负载均衡支持、Pool 连接池全量刷新、Guzzle 持久化 Cookie 以及数据库 JSON 包含键查询等核心优化特性。
推荐顺序:优先用 Request / Response 属性 → 次选用 Context → 尽量避免自定义全局/静态存储。
- 通过请求对象传递:
$request->withAttribute('user', $user),后续中间件或控制器用$request->getAttribute('user') - 用协程上下文:
Context::set('user', $user),取值用Context::get('user') - 若需跨协程(如子协程),
Context::copy()显式传递上下文 - 绝对不要在单例类里用
protected或private成员变量存请求数据
如何快速检查项目里是否已存在这类风险
别靠肉眼扫,用 grep 快速定位高危模式:
grep -r '\$this->[a-zA-Z0-9_]\+ =' app/ --include="*.php" | grep -E "(= \$_|->getAttribute|->input\(|->post\()"
更关键的是检查构造函数和process/handle方法中是否有赋值语句;另外留意protected数组类属性(如$errors、$data)是否在方法中被修改。
真正难排查的,往往不是明显写死的$this->uid,而是某个工具类里一个不起眼的protected $buffer,在多次调用中悄悄累积、覆盖——这种隐性状态污染,才是线上偶发 bug 的温床。










