hyperf常驻内存模型导致static $instance跨请求污染,因worker进程长期存活且协程共享同一块静态内存;安全方案为协程uid隔离缓存或di容器设置shared=false。

Hyperf 的常驻内存模型不是“替换”传统请求销毁机制,而是彻底取消它——Worker 进程不重启,static 变量、单例对象、全局缓存全都不释放。直接照搬 PHP-FPM 写法,static $instance 就会跨请求污染,接口一并发就返回错乱数据。
为什么 static $instance 在 Hyperf 里必然污染
PHP-FPM 每次请求新建进程,static 变量随进程消亡;Hyperf 的 Worker 进程持续运行,static $instance 绑定在进程地址空间,所有协程共享同一块内存。协程 A 调用 setUser(1001),协程 B 紧接着调用 getInstance(),拿到的是已被篡改的旧实例,不是新对象。
- 污染无法靠
php bin/hyperf.php reload清除,必须kill -USR1重启 Worker 才重置 -
__destruct()不会被触发,因为对象没被销毁 - 哪怕加了
unset($instance),也只是清掉当前协程栈里的引用,static变量本身还在进程内存里
协程安全的单例获取方式
不能靠 static 做“单例”,得做“协程本地单例”。两种可靠路径:
- 用
Swoole\Coroutine::getUid()构建隔离键:$key = 'db_' . Swoole\Coroutine::getUid(),缓存容器必须是局部变量(如方法内$cache = []),不能是static $cache - 交给 DI 容器控制生命周期:在
@Inject注解里加shared=false,或调用$container->make(DbConnection::class, [], true),每次获取都是全新实例 - 验证是否生效:两个并发协程打印
$container->get(DbConnection::class)->getUid(),ID 不同即隔离成功
哪些写法看着像隔离,实际仍危险
很多看似“协程友好”的写法,底层仍是进程级共享:
-
Co::getContext()返回的是协程上下文,但若把static $cache存进去,还是共享的——因为Co::getContext()本身不解决静态变量生命周期问题 - 用
ApplicationContext::getContainer()获取容器再get(),如果该类配置为shared=true(默认),依然复用单例 - 在
beforeRequest或afterRequest生命周期里手动unset静态变量,无效——static变量不属于请求生命周期
真正要盯住的不是“怎么写”,而是“谁拥有这块内存”:Worker 进程不死,所有绑定到它的变量就一直活着。协程隔离不是加个 go() 就自动生效,得从变量声明位置、容器配置、缓存键设计三个层面同时切断进程级共享链路。











