hyperf中$this->userid会污染其他请求,因为worker进程常驻内存且服务类默认单例,实例属性被所有协程共享,导致后一个请求读取到前一个请求残留的用户数据。

不能用类属性、静态变量或全局数组存请求级数据,否则必然出现数据串流和内存泄漏。
为什么 $this->userId 会污染其他请求
Hyperf 的 Worker 进程常驻内存,中间件、服务类默认是单例。一旦在 $this->userId 这类实例属性里写入数据,它就留在进程内存里,下一个协程复用该对象时直接读到上一个用户的值——不是“可能出错”,是“一定出错”。
常见错误场景:
- 登录中间件里把
$request->getAttribute('uid')赋给$this->uid,后续所有方法都读这个属性 - 服务类构造函数里缓存
$_SERVER['REQUEST_TIME_FLOAT'],结果所有请求共享同一个时间戳 - 用
static $cache = []做“简易本地缓存”,协程切换后键值被覆盖或无限增长
$_GET、$_POST 等超全局变量还能用吗
不能直接用。原生 $_GET 在协程间不隔离,多个请求同时修改会互相覆盖。Hyperf 提供了协程安全的代理层,但必须通过框架入口获取:
- 正确方式:用
$request->getQueryParams()或$request->getParsedBody(),它们底层走Context::get()隔离 - 错误方式:直接读
$_GET['id'],尤其在异步任务或子协程中,大概率拿到空或错的数据 - 如果非要封装一层“全局访问”,必须手动绑定上下文:
Context::set('my_get', $request->getQueryParams()),且每次请求初始化
Context::set() 和 $request->withAttribute() 选哪个
优先用 $request->withAttribute()。它只在当前请求生命周期有效,随 ServerRequestInterface 流转,天然防泄漏;而 Context::set() 是纯协程级存储,若在定时任务、异步投递等非 HTTP 上下文中误用,容易脱离预期作用域。
- HTTP 请求处理中:用
$request->withAttribute('user_id', $uid),后续中间件或控制器用$request->getAttribute('user_id') - 纯协程任务(如
go(function () { ... })):才考虑Context::set('task_id', $id),且必须配对Context::get() - 绝对不要混用:比如在中间件里
Context::set(),控制器里却去读$_SESSION—— 完全不同机制,数据根本不通
Redis 存储大对象时的隐性内存风险
表面看是 Redis 内存爆了,实际常驻进程里的 PHP 对象引用没释放才是根因。比如把未压缩的 5MB 数组塞进 Redis,json_encode() 生成的字符串在 PHP 内存里先占一份,再经 gzdeflate() 又占一份,最后传给 Redis 客户端又复制一次 —— 协程结束前这些临时字符串不会被 GC。
- 写入前强制校验:
if (strlen($value) > 512 * 1024) { throw new RuntimeException('Value too large'); } - 禁用
serialize(),改用json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) - 压缩后必须
base64_encode(),否则二进制内容可能破坏 RESP 协议解析 - key 名加
:gz标记,避免读取时漏解压,导致业务拿到乱码还继续往下跑
最易被忽略的一点:协程退出 ≠ 内存立即释放。PHP 的 GC 不保证即时回收,尤其是存在闭包引用、静态属性间接持有、或 Redis 客户端内部缓存时,残留对象可能挂住几 MB 内存,持续几十秒甚至更久。观察 memory_get_usage(true) 比看 RSS 更准。











