Hyperf协程中$this->userId等成员变量会串数据,因Service/Controller默认单例且协程不重置私有属性;Context仅限当前协程有效,子协程需显式copy,task_worker中完全失效;HTTP场景优先用$request->withAttribute()而非Context。

Hyperf 协程中跨请求读到别人的 $this->userId 或 static $cache,不是代码写错了,是默认行为——所有协程共享单例实例和静态内存,不隔离就是常态。
为什么 $this->xxx 在协程里会串数据
Hyperf 容器默认把 Service、Controller 注册为单例(shared=true),Worker 进程启动后只初始化一次对象。协程切换时,PHP 不重置 protected / private 成员变量,上一个请求写入的 $this->user_id = 1001,下一个协程直接读到,毫无预警。
- 典型高危字段:
$this->cache、$this->errors、$this->requestId、$this->token - 哪怕加了
@Inject,只要没显式写@Inject(shared=false),就仍是同一个实例 - 错误示范:
class UserService { private $userId; public function setUserId($id) { $this->userId = $id; } }—— 这个$userId是进程级“公共白板”
为什么 Context::set() 不是万能的
Context::set('user_id', $uid) 确实绑定到当前协程 ID,但它的安全边界非常明确:只对当前协程有效,不自动穿透子协程、不跨 task_worker、不进定时任务。
- 子协程必须显式调用
Context::copy()才能拿到父协程数据,漏掉就Context::get('user_id')返回null - 投递到
task_worker时,Context完全失效——因为 task 是独立进程,连内存地址都不一样 - 不要存大对象:
Context::set('order', $order)可能引发序列化失败或内存膨胀,只放轻量元数据(user_id、trace_id、locale)
Request 属性比 Context 更适合 HTTP 场景
在中间件或 Controller 中处理请求时,优先走 $request->withAttribute(),而不是绕一圈去 Context。
-
$request->withAttribute('user', $user)后,后续所有中间件、控制器都能用$request->getAttribute('user')安全取值,天然绑定请求生命周期 -
Request对象本身由框架按协程隔离创建,不会被其他请求污染 - 别混用:
Context::set('user', $user)和$request->getAttribute('user')是两套存储,不互通;选一个并坚持到底
哪些地方必须手动清理或禁用共享
静态变量、单例属性、全局数组,不是“慎用”,是“禁用”。真要复用状态,得靠容器配置或显式传递。
- 查污染:全局搜索
protected $、private $、static $,再 grep->cache\[、->user、->errors\[\] - 改单例为非共享:
@Inject(shared=false)或容器配置里设'shared' => false - 废除
$_SESSION:SessionInterface必须 DI 注入,不能全局访问 - CLI 命令/单元测试里
Context会退化为静态数组,此时若依赖它做隔离,实际已失效
最易被忽略的一点:协程上下文不是“开关一开就自动隔离”,而是“你写了 set 才有,你没 copy 就没有,你投递到 task 就彻底消失”。它不替你做决定,只提供隔离能力——怎么用,还是得自己盯住每一条调用链。


















