协程变量污染源于hyperf单例服务在worker进程内共享成员变量,导致跨请求数据混用;应使用context::set/get绑定协程上下文、避免成员变量存状态、注意子协程需手动透传context。

协程变量污染的典型现象
请求 A 的 $this->user_id = 1001,请求 B 却读到 1001 而不是自己的 4567;缓存数组 $this->cache 越滚越大,不同用户数据混在一起;日志里看到“用户 4567 修改了用户 1001 的订单”——这不是逻辑错,是变量被污染了。
根本原因就一条:Hyperf 默认注册的服务(Controller、Service、Middleware)全是单例,整个 Worker 进程只初始化一次,而协程切换时 不会自动重置对象属性。成员变量就是进程级共享的,不是请求级隔离的。
快速定位污染源的三步法
别一上来就翻代码,先用最小成本确认是不是变量污染:
- 在疑似污染的类(比如
UserService)构造函数里加日志:$this->logger->info('UserService created', ['coroutine_id' => \Swoole\Coroutine::id()]);—— 如果一个实例被多个协程共用,你会看到同一个对象 ID 出现在不同协程日志里 - 在方法开头和结尾打印关键属性值:
$this->logger->debug('Before getUser', ['user_id' => $this->user_id, 'coroutine_id' => \Swoole\Coroutine::id()]); - 用
\Hyperf\Context\Context::get('request_id')对比:如果两个日志的request_id不同,但user_id相同,基本坐实污染
哪些写法最容易中招
以下代码在压测或并发稍高时几乎必现问题,尤其注意带 protected 或 private 成员变量的写法:
-
protected $currentUser;+ 在init()里赋值 → 后续所有协程都读这个残留值 -
protected $errors = [];+ 每次调用$this->errors[] = 'xxx';→ 数组越堆越多,不清理 -
protected $cache = [];+ 缓存未绑定请求上下文,直接按 ID 存取 → A 请求写入,B 请求命中并返回 A 的数据 -
static $counter = 0;+ 静态变量完全跨协程共享,++操作非原子,结果不可预期
特别提醒:__construct() 只执行一次,__destruct() 在 Worker 关闭时才触发,**它不会在每次请求结束时调用**。
安全替代方案必须绑定协程上下文
所有“属于当前请求”的状态,必须脱离对象实例,落到协程上下文里:
- 用
\Hyperf\Context\Context::set('user_id', $id)写,\Hyperf\Context\Context::get('user_id')读 —— 安全、隔离、无需清理 - 缓存改用
Co\Channel或\Hyperf\Cache\CacheManager(带 key 前缀 + TTL),别手写成员数组 - 临时状态不要存在
$this->xxx,改用方法参数传递或局部变量 - 若必须复用类实例,注册为
singleton以外的作用域(如prototype),但注意性能开销
最常被忽略的一点:Context 数据不会自动透传到子协程(比如 Coroutine::create() 启动的新协程),需要手动 Context::copy() 或显式传递。这点在异步任务、定时器回调里极易出错。











