单例Service中存$this->userId会导致用户数据串扰,因协程共享对象且PHP不自动重置成员变量;应改用Request属性、Context或函数参数传递请求级数据。

Hyperf 3.1 面试中常被追问:为什么在单例 Service 里存 `$this->userId` 会导致张三的订单显示在李四的接口响应里——这并非逻辑错误,而是协程常驻内存下全局变量污染的典型表现,必须用协程上下文隔离而非类属性承载请求级数据。
为什么单例类的成员变量会串数据
Hyperf 默认将 Service、Controller、Middleware 注册为单例,Worker 进程启动后只初始化一次实例,所有协程共享该对象。
协程切换时,【PHP 不会自动重置对象的 protected/private 成员变量】,上一个协程写入的 `$this->cache[1001] = [...]`,下一个协程读取时仍存在,且值是前者的残留数据。
这就像办公室共用一台复印机,A 员工设置双面打印后没还原,B 员工按默认走纸就直接双面输出——成员变量就是那个未清空的“机器状态”。
静态变量 `static $counter++` 更危险:它连协程层级都不区分,整个进程内完全共享。
安全存储请求级数据的三种方式(按推荐顺序)
方法一:优先使用 Request 对象属性传递
在中间件中调用 `$request->withAttribute('user_id', $uid)`,后续控制器通过 `$request->getAttribute('user_id')` 获取。Request 是协程安全的,其 attribute 由框架自动绑定到当前协程生命周期。
方法二:显式使用协程上下文 Context
写入:`Context::set('user_id', $uid)`;读取:`Context::get('user_id')`。这是最通用、最可控的方式,尤其适合跨中间件、跨服务调用场景。
方法三:通过函数参数逐层传递
把 `$uid` 作为参数传给 `UserService::getUser(int $uid)`,不依赖任何外部状态。干净、可测试、无副作用,但对深层调用链略显冗长。
检查项目中是否已存在污染风险
第一步:全局搜索 PHP 文件中所有 `protected $` 和 `private $` 开头的成员变量定义,重点筛查含 cache、user、token、data、errors、requestId 等关键词的字段。
第二步:对命中类执行 grep -r “->cache\[” “->user” “->errors\[\]” 等写入操作,确认是否存在未清理的赋值行为。
第三步:运行压测脚本(如 ab -n 100 -c 10),构造两个不同用户 ID 的并发请求,观察响应体中是否出现用户数据错乱——这是最直接的验证手段。
注意:【不要依赖单元测试覆盖判断】,单测通常在单协程中运行,无法复现多协程竞争场景。
Hyperf 3.1 特别注意事项
DI 容器已放弃对无类型提示构造参数的自动解析,如 `public function __construct($db)` 将直接报错 `Entry "X" does not exist`。必须全部改为 `public function __construct(private Db $db)` 或 `public function __construct(DbInterface $db)`。
属性注入正式成为一等公民,但 `#[Inject]` 只支持 public/protected 属性,private 属性不生效;且注入发生在构造函数执行之后,不能用于替代构造时的强依赖。
升级到 3.1 后若发现服务注入失败,先检查 `composer dump-autoload` 是否执行,再确认所有构造函数参数是否带明确接口或类类型声明。


















