hyperf中不能用静态变量或类属性存请求数据,因service/middleware默认单例,协程共享同一实例,成员变量/静态变量不隔离,导致并发时用户数据串号。

Hyperf 里直接用 $GLOBALS、static 或类属性存请求数据,必出错——不是“可能”,是“一定”会在并发时串数据。
为什么不能用静态变量或类属性存用户信息
Hyperf 的 Service 和 Middleware 默认是单例,一个实例被所有协程共享。你往 static $user_id 里写一次,下个协程读到的可能是上个用户的值;哪怕只是临时赋值 $this->uid = $request->getAttribute('uid'),也会被下一个请求覆盖。
- 错误现象:A 用户登录后,B 用户接口返回 A 的订单列表
- 根本原因:PHP 进程常驻,协程复用同一对象实例,成员变量/静态变量不隔离
- 对比验证:加
var_dump(getmypid(), Coroutine::id())就会发现,不同请求的getmypid()相同,但Coroutine::id()不同 —— 这正是上下文必须按协程隔离的依据
Context::set() 和 Context::get() 的正确调用时机
上下文不是“设了就能一直用”,它只在当前协程生命周期内有效,且父子协程默认不自动继承(除非显式传递)。中间件里 Context::set() 必须在 $next($request) 之前完成,否则下游控制器拿不到。
- 常见错误:在
$next()后才Context::set('trace_id', ...),结果日志里 trace_id 总是null - 子协程场景:调用
Coroutine::create()启动新协程时,需手动Context::copy()或传入父上下文快照,否则子协程读不到Context::get('user_id') - 安全写法示例:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $userId = $request->getAttribute('user_id'); Context::set('user_id', $userId); // ✅ 必须在 $next 前 return $handler->handle($request); }
避免 Context::override() 的误用陷阱
Context::override() 看似强大,但容易引发“覆盖丢失”或“无限递归”——它不是简单赋值,而是用回调函数重新生成值,并替换整个键。若回调里又调用了 Context::get(),可能触发死循环。
- 典型误用:
Context::override('config', fn($old) => array_merge($old ?? [], ['timeout' => 5])),当$old为null时,array_merge(null, [...])会报错 - 更稳妥的写法:
Context::set('config', array_merge(Context::get('config', []), ['timeout' => 5])) - 真正适合
override的场景:需要原子性更新(如计数器累加)、或依赖当前值做条件判断(如仅当未设置时初始化)
Context 键名命名要带业务前缀,禁止裸用通用名
直接用 Context::set('id', 123) 是高危操作。不同模块、中间件、SDK 都可能用相同键名,导致覆盖。Hyperf 自身就用了 server_request、response 等内部键,冲突会破坏框架行为。
- 推荐格式:
Context::set('auth.user_id', $uid)、Context::set('api.version', 'v2') - 避免:
Context::set('user_id', $uid)(太泛,易被其他组件覆盖) - 调试技巧:开发期可用
var_dump(Context::all())查看当前协程所有上下文键,快速定位污染源
Context 不是万能胶,它解决的是“协程内数据隔离”,而不是“跨进程状态同步”。如果真需要全局共享(比如配置热更新),该走 Redis 或 ETCD,别硬塞进 Context。











