Hyperf协程数据隔离需主动避免全局变量、静态属性和单例对象属性复用;正确方式是使用RequestInterface注入请求数据、Context::set/get存储协程局部状态,并严格分离读写连接池。

Hyperf协程间数据隔离不是“默认就安全”,而是必须主动做对三件事:不用全局变量、不依赖静态属性、不复用对象实例属性——否则高并发下必然串数据。
为什么 $_GET / static / $this->user_id 会串数据
协程共享 Worker 进程内存空间,$_GET 是 PHP 全局变量,Swoole 不注入请求上下文;static 是进程级单例,所有协程共用同一块内存;$this->user_id 所在的 UserService 默认是 DI 单例,实例只初始化一次,属性自然跨协程污染。
常见错误现象:
- A 用户登录后设
$this->user_id = 1001,B 用户请求进来直接读到1001 - 中间件里
var_dump($_GET)输出的是上一个请求残留的参数 - 异步任务中日志打出来的 trace_id 和主请求不一致
正确获取请求数据:用 RequestInterface 而非 $_GET
控制器方法参数声明即可自动注入,框架保证每次都是当前协程的干净请求上下文。
示例:
public function index(RequestInterface $request)
{
$id = $request->get('id'); // ✅ 安全
$name = $request->input('name');
}
工具函数或中间件里不能靠 global 或 $_GET,必须显式从协程上下文取:
- 用
Context::get(ServerRequestInterface::class)拿原始请求对象 - 或在调用处把
$request作为参数透传进去 - 绝对不要在非控制器作用域里写
$_GET['xxx']
协程内状态存储:用 Context::set/get,别碰 static 和 $this
需要临时存用户 ID、trace_id、权限标识等轻量元数据时,唯一安全方式是 Context::set() 和 Context::get()。
示例:
// 登录成功后
Context::set('user_id', 123);
Context::set('token', 'abc');
// 后续任意位置(包括中间件、服务类、异步任务)
$user_id = Context::get('user_id', null); // ✅ 绑定当前 CID
注意:
-
Context::set()写入的数据只对当前协程 CID 有效 - 父子协程不自动继承,子协程需手动复制:
go(function () use ($ctx) { Context::set('user_id', $ctx['user_id'] ?? null); }, Context::all()); - RPC 场景优先用
RpcContext::set(),它已内置透传逻辑
多连接池混用时的上下文污染风险
在同一个协程里交叉使用 DB::transaction() 和 Db::connection('read_pool_1'),会导致 MySQL 连接泄漏、事务静默回滚、WaitTimeoutException 等问题。
根本原因:
-
DB::transaction()把连接绑定到当前协程上下文 -
Db::connection()尝试从另一池取连接,但无法接入事务生命周期 - 两个连接同时被当前协程持有,连接池只回收显式归还的连接
正确做法:
- 写操作走独立协程:
go(function () { Db::connection('write')->insert(); }); - 读操作另起协程:
go(function () { Db::connection('read_pool_1')->select(); }); - 结果聚合用
Co\WaitGroup或Co\Channel,不要轮询或 sleep
协程上下文不是“用了就安全”的黑盒,它是有明确边界和生命周期的——每个 go() 都是一个新 CID,每次 Context::set() 都只写进当前 CID。漏掉一次透传、多写一个 static、少加一个 use ($ctx),都可能在压测时突然暴雷。


















