hyperf单例本身不失效,但协程中误用类属性、静态变量或context未defer会导致数据污染;正确做法是将请求级数据存入协程上下文并配defer释放。

Hyperf 中的单例(比如 Container::get() 返回的类实例)本身不会“失效”,但你在协程中误用类属性、静态变量或上下文,会让它表现得像“不是单例”——本质是数据污染,不是容器失灵。
单例对象还在,但它的属性被其他协程改了
Hyperf 的绝大多数服务(如 UserService、RedisFactory)确实是单例注册的,但单例 ≠ 线程/协程安全。一旦你在类里存了请求级数据,就等于把不同协程的脏数据塞进了同一个对象里。
- 错误写法:
class UserService { public $currentUserId; public function setCurrent($id) { $this->currentUserId = $id; } }—— 多个协程调用setCurrent()会互相覆盖 - 正确做法:把
$currentUserId存进协程上下文,用Context::set('user.id', $id),每次取都带协程隔离 - 特别注意:Controller 是单例,
$this->request看似是属性,实则是每次从 Context 动态获取的代理,不能反向依赖它来存自定义状态
静态变量在协程间共享,根本不是隔离的
PHP 的 static 变量生命周期绑定进程,不随协程销毁而清理。哪怕你只在一个协程里写了 self::$cache[] = $data,下次任意协程进来都能看到它。
- 典型泄漏点:
public static $tokenCache = [];+ 每次请求往里push,没做 key 隔离或清理 - 验证方式:在关键方法开头加
var_dump(count(self::$tokenCache));,看是否随请求数线性增长 - 替代方案:用
Context::set('token.cache', $data),协程退出自动释放(前提是没漏掉defer)
Context::set() 用了但没配 defer,协程结束也不清
Context::set() 本身不保证自动清理。如果你存的是大数组、资源句柄或闭包,又没显式 defer,协程结束了,这些值还挂在 Swoole 的协程上下文结构体里。
- 危险写法:
Context::set('big_data', file_get_contents('/huge.json'));—— 内存不释放,协程数一多直接 OOM - 安全写法:
Coroutine::defer(fn() => Context::destroy('big_data'));或在业务逻辑末尾手动Context::destroy() - 检查手段:在中间件里临时加
var_dump(Co::stats()['coroutine_num'], Context::has('big_data'));,确认协程数涨但上下文残留
别用 ps aux 看内存就断定是单例问题
Worker 进程内存持续上涨,第一反应不该是“单例坏了”,而是先确认是不是协程上下文或静态变量在吃内存。Swoole 的 Co::list() 和 Co::stats() 才是更直接的证据:
- 执行
var_dump(Co::stats());—— 如果'coroutine_num'居高不下(比如 > 100),说明协程没正常退出,上下文大概率堆积 - 执行
var_dump(array_keys(Co::list()));—— 拿到活跃协程 ID,再结合Context::get()查具体某个协程存了什么 - 注意:
Co::list()返回的是整型 ID 列表,不是字符串,别用Context::get('key', null, '123')这种传错类型的写法
真正难排查的,是那些没报错、没日志、但每次请求都悄悄往静态数组里塞一条记录的代码——它不崩溃,只是让整个 Worker 在你没察觉时慢慢变重。











