hyperf中static变量必然污染,因worker常驻且协程共享静态内存;context::set须在$next()前调用;单例类成员变量同样危险;修复优先级为request属性、context、参数传递或非共享实例。

Hyperf里static变量为什么一定污染
因为Worker进程常驻,所有协程共享同一块静态内存。你写static $user_id = 1001,下一个协程读到的还是1001,哪怕它属于用户1002——这不是“偶发”,是必然行为。
常见错误现象包括:
- 接口返回其他用户的订单列表
- Redis缓存键写入后被后续请求覆盖
- 数据库事务跨请求提交,导致数据错乱
根本原因:Swoole不重置静态变量,PHP也不做协程级隔离。FPM模式下每个请求是新进程,静态变量天然清空;Hyperf里,static就是全局变量。
Context::set()必须在$next()前调用
中间件中Context::set('user_id', $uid)如果放在$handler->handle($request)之后,下游控制器永远拿不到值——上下文只绑定当前协程,且不会自动向子协程传递。
安全写法示例:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$userId = $request->getAttribute('user_id');
Context::set('user_id', $userId); // ✅ 必须在这行
return $handler->handle($request);
}
容易踩的坑:
- 在
$next()后才Context::set(),日志里trace_id总是null - 调用
Coroutine::create()启动新协程时没Context::copy(),子协程读不到父协程设的user_id - 误用
Context::override(),回调里又调Context::get()引发死循环
单例类成员变量比static更隐蔽但一样危险
Hyperf默认把@Inject类注册为单例,一个实例被所有协程复用。$this->cache或$this->errors不是“每个请求一份”,而是“所有人共用一张工作台”。
排查建议:
- 全局搜索
protected $和private $,重点看含cache、user、token、data的字段 - 对命中类再
grep -r "->$cache\[" "->$user"确认是否有未清理的写入 - 压测时构造两个不同
user_id并发请求,直接观察响应体是否错乱
修复方式优先级:
- 第一选:
$request->withAttribute('user_id', $uid)→$request->getAttribute('user_id') - 第二选:
Context::set('user_id', $uid)→Context::get('user_id') - 第三选:函数参数显式传递,如
UserService::getUser(int $uid)
让容器返回非共享实例的两种写法
不想改业务逻辑?可以直接让DI容器每次返回新实例,彻底绕过成员变量污染问题。
方式一(注解):
#[Inject(shared: false)]
class UserService
{
}
方式二(运行时):
$service = $container->make(UserService::class, [], true); // 第三个参数true = shared=false
验证是否生效:
- 两个并发请求中打印
$container->get(UserService::class)->getUid(),ID不同即成功 - 注意:
@Inject默认等价于make($class, [], true),不加shared=false就仍是单例
真正麻烦的不是怎么写对,而是那些已经在线上跑了一年、靠static $instance撑起来的旧服务——它们不会报错,只会悄悄串数据。上线前务必压测验证,别信“看起来没问题”。











