Hyperf 不是简单换启动方式,而是执行模型从进程级切换为协程级隔离,因此 $_GET、static、$this->user_id 等写法会导致高并发数据串流;正确做法是用 RequestInterface 获取参数、DI 容器管理服务、Context::set/get 传递轻量元数据,并显式透传父子协程上下文。

Hyperf 不是“把老代码换个启动方式就能跑”,而是整个执行模型从进程级隔离切换到了协程级隔离——这意味着你不能再依赖 $_GET、static、$this->user_id 这类“看起来能用”的写法,否则高并发下必然串数据。
为什么 $_GET / $_POST 在 Hyperf 里取不到
不是框架删了它们,是 Swoole 协程运行时根本没给 PHP 全局变量注入请求上下文。多个协程共享同一块内存空间,$_GET 被上一个协程写过,下一个协程读到的就是脏值。
- 正确做法:在控制器方法参数中声明
RequestInterface $request,再调用$request->get('id')或$request->input('name') - 工具函数里要用?必须显式从协程上下文拿:
Context::get(ServerRequestInterface::class),不能靠global或$_GET - 中间件里也别直接读
$_GET,哪怕只为了临时调试——它返回的可能是前 5 个请求中任意一个的残留数据
static 单例和 $this->xxx 属性为什么导致用户数据串流
PHP 的 static 是进程级的,Hyperf Worker 进程常驻内存,所有协程共用同一个 static $instance;而 $this->user_id 是对象实例属性,UserService 默认单例,实例只初始化一次,属性自然跨协程共享。
- 后果:A 用户登录后设
$this->user_id = 1001,B 用户的请求进来直接读到 1001,订单、权限、日志全错乱 - 正确替代:用 DI 容器获取服务:
ApplicationContext::getContainer()->get(UserService::class),Hyperf 默认按协程生命周期管理实例 - 若需手动存状态,用
Context::set('user_id', 123)+Context::get('user_id', null),别碰static和实例属性
父子协程之间 Context 数据不自动传递
每个协程有独立 ID,Context::set() 写入的数据绑定在当前 CID 上,子协程 CID 不同,查不到父协程写的内容——这不是 bug,是为避免高并发下上下文污染做的主动隔离。
- 简单场景:在
go()前用Context::copy()把当前上下文传进去:go(function ($ctx) { Context::set('user', $ctx['user'] ?? null); }, Context::all()); - 多层嵌套或异步任务:改用根协程 ID 存储,先定义
getRootCid()函数,再统一以Context::set(getRootCid(), 'user_id', 123)写入 - RPC 调用链:用
RpcContext::set(),服务端直接RpcContext::get(),中间件自动透传,不用手动序列化
协程里别用 sleep()、file_get_contents()、curl_exec()
这些函数未被 Swoole Hook,一调就阻塞整个协程,其他并发请求全部卡住——QPS 不升反降的头号原因。
- 必须确保启动方式是
php bin/hyperf.php start(走 Swoole Server),而非普通 CLI - 不确定是否协程安全?优先用 Hyperf 封装的客户端:
Hyperf\HttpClient\Client、Hyperf\Redis\Redis、Hyperf\Database\Connection - 连数据库都别用原生
mysqli或PDO,连接池复用机制只对Hyperf\Database生效
最易被忽略的一点:协程上下文不是万能抽屉,Context::set() 只适合放轻量元数据(user_id、request_id、trace_id),别存整个订单对象或大数组——它不释放内存,协程结束才清,容易引发内存缓慢泄漏。











