hyperf 3.1 中单例服务成员变量跨请求污染是协程共享实例导致的必然竞态问题;因容器默认返回进程级单例,所有协程共用同一对象,$this->currentuser 等属性被并发读写引发状态泄露。

Hyperf 3.1 中多个协程并发访问同一单例服务时,成员变量值在不同请求间意外串改,日志 trace_id 混乱、用户数据错乱、缓存命中率骤降——这不是偶发 Bug,而是协程共享内存模型下必然发生的竞态条件。
为什么单例类的 protected 成员变量在 Hyperf 中会跨请求污染
第一步:打开一个 Hyperf 项目,在 UserService 类中定义 【protected $currentUser = null】,并在 handle 方法中赋值 $this->currentUser = $user;
第二步:用 ab 或 wrk 同时发起 100 个并发请求,每个请求携带不同 user_id;
第三步:在响应中打印 $this->currentUser['id'],你会看到大量非当前请求的 user_id 出现在响应里;
原因很直接:Hyperf 默认容器返回的是进程级单例,所有协程共用同一个对象实例;而 PHP 的对象属性在 Swoole Worker 进程内是全局可见的,协程 A 写入 $this->currentUser,协程 B 下一秒就读到了——这不是线程安全问题,是协程隔离缺失导致的状态泄露。
检查项目是否已存在协程不安全状态存储
方法一:grep -r '$this->cache\|$this->errors\|$this->requestId' app/ --include="*.php";
方法二:在中间件中插入检测逻辑:var_dump(spl_object_hash($this), Coroutine::getuid());,若同一对象 hash 在不同协程 ID 下反复出现,即为共享实例;
方法三:在 onWorkerStart 里初始化一个静态计数器 static $initCount = 0; $initCount++,然后在任意控制器中 echo $initCount;若并发请求中该值持续增长而非恒为 1,说明类被重复构造——但更危险的是它恒为 1 且状态被污染。
安全存储请求级数据的三种落地方式
方法1:优先绑定到 Request 对象
在中间件中调用 $request = $request->withAttribute('user', $user),后续控制器通过 $request->getAttribute('user') 获取;【此方式天然协程隔离,无需额外清理】;
方法2:使用 Context 显式存取
写入:Context::set('auth_token', $token);读取:$token = Context::get('auth_token', null);若需子协程继承,必须显式调用 Context::copy() 传递上下文,否则子协程无法访问父协程设置的值;
方法3:注入协程局部作用域的闭包依赖
在服务提供者中注册:$container->singleton(LoggerInterface::class, fn() => new Logger(Coroutine::getuid()));让每个协程获得专属实例,避免任何共享状态;











