hyperf中静态变量和单例成员变量会导致协程间数据污染,因所有协程共享同一进程内的静态内存和单例实例;安全做法是优先用request属性、次选context上下文、必要时显式传递协程上下文。

Hyperf 3.1项目中出现协程间数据污染、请求状态错乱、缓存返回错误用户信息,八成是静态变量或单例类成员变量惹的祸。
为什么静态变量在Hyperf协程里会串数据
第一步:打开你的服务类,找到类似 private static $counter = 0; 或 protected static $cache = []; 的定义。
第二步:确认这个类被容器管理(比如加了 @Inject 注解),且未显式设置 shared=false。
第三步:运行压测或并发请求,观察不同请求是否读取/修改了同一份静态数据——【所有协程共享同一块静态内存空间,没有隔离】,哪怕你用的是 make(UserService::class),只要类里用了 static,就全协程共用一个值。
这和 PHP-FPM 模式完全不同:FPM 每个请求是独立进程,静态变量天然隔离;而 Hyperf 协程跑在同一个进程里,静态变量就是全局变量。
单例模式下成员变量为何不安全
方法一:检查构造函数是否私有、是否含 self::$instance、是否通过 getInstance() 获取实例——这些本身没错,但错在把请求级数据塞进实例属性。
方法二:定位到类似这样的代码:$this->currentUser = $user; 或 $this->errors[] = 'xxx';,只要它出现在单例类(尤其是容器注入的单例)里,就危险。
【单例实例在协程间复用,$this 指向同一个对象,成员变量会被所有协程反复覆盖】。哪怕你调用一百次 make(UserService::class),只要没加 shared=false,拿到的仍是同一个实例。
注意:Hyperf 容器默认 shared=true,@Inject 等价于 make($class, [], true),不是“每次 new”。
协程安全地存请求数据的三步法
① 优先绑定到 Request 对象:$request->withAttribute('user', $user),后续中间件或控制器用 $request->getAttribute('user') 取值。
② 次选 Context 上下文:Context::set('auth_token', $token),取值时用 Context::get('auth_token'),简单直接,无需传参。
③ 跨子协程必须显式传递:$newContext = Context::copy($currentContext); Coroutine::create(function () use ($newContext) { Context::set($newContext); });——不 copy 就拿不到父协程设的值。
这三步不依赖任何类实例或静态变量,完全由协程调度器保障隔离性,是唯一真正安全的路径。
如何让容器返回非共享实例
方式一(注解):在类上加 @Inject(shared=false),该类每次 make() 都新建对象,彻底规避成员变量污染。
方式二(代码):手动调用 make(UserService::class, [], false),第三个参数 false 强制禁用共享。
方式三(配置):在 config/autoload/dependencies.php 中声明:UserService::class => [UserService::class, false],全局生效。
⚠️ 一旦启用 shared=false,务必确认该类无昂贵初始化逻辑(如连接数据库),否则会显著增加开销。











