协程上下文必须用 context::set()/get() 传递请求级数据,禁用 $this->xxx 等共享变量;子协程需手动透传上下文;中间件应避免单例复用成员变量;context key 需加命名空间前缀防冲突。

协程上下文没用 Context::set,数据直接串流
Hyperf 里最典型的用户信息错乱,就是中间件或服务里直接给 $this->user 赋值,然后在后续协程里读取——这等于把数据挂到了对象实例上,而单例中间件/服务会被多个协程复用,$this->user 就成了共享变量。A 协程写入,B 协程读到,张三看到李四的订单,不是玄学,是必然。
正确做法只有一条:所有跨协程传递的请求级数据,必须走 Context::set() 和 Context::get()。
- 中间件中存用户:
Context::set('user_id', $request->getAttribute('user_id')) - 任意服务中取用户:
$userId = Context::get('user_id') ?? null - 不要依赖
$this->xxx、static::$xxx、全局变量、单例类属性
异步任务里拿不到主请求的上下文
用 Coroutine::create() 或 AsyncTask 提交子任务时,协程上下文默认不会自动继承。子协程启动时,Context::get() 返回的是空,不是父协程里的 user_id 或 trace_id——日志断链、权限校验失败、数据归属错误,全由此起。
必须显式透传,不能靠“它应该有”。两种安全方式:
- 手动捕获再注入:
$userId = Context::get('user_id'); Coroutine::create(function () use ($userId) { Context::set('user_id', $userId); /* 后续逻辑 */ }); - 用
Co::defer()或Co::run()包裹时注意:它们不自动继承上下文,仍需手动 set - 避免在
AsyncTask::execute()里直接调Context::get(),除非你确认上下文已由框架自动携带(如通过Job组件提交的任务)
中间件生命周期配置错误,导致成员变量被复用
Hyperf 默认把中间件注册为单例(singleton),意味着整个进程生命周期内只有一个实例。如果你在中间件里定义了 protected $authData,那它就真成了“全局变量”——不同请求的协程会反复读写同一块内存。
解决路径很明确:
- 查
config/autoload/middlewares.php,确认中间件是否被设为singleton => false - 更推荐的做法:彻底删除成员变量,改用
Context或$request->withAttribute() - 如果必须用类属性,确保该中间件配置为每次请求新建实例:
Container::setSingleton(YourMiddleware::class, function () { return new YourMiddleware(); });不推荐,冗余且易漏
Context::set 嵌套调用时 key 冲突或覆盖
多人协作时容易忽略一点:Context 是扁平键值对,没有命名空间。A 模块用 Context::set('user', ...),B 模块也用 Context::set('user', ...),后者会直接覆盖前者——尤其在嵌套中间件、组合服务调用时高频发生。
防冲突的实操习惯:
- 统一前缀,比如
Context::set('auth.user_id', $id)、Context::set('trace.request_id', $rid) - 避免通用名:
user、data、config这类 key 必须加域前缀 - 关键业务字段建议封装成常量:
const CONTEXT_USER_ID = 'auth.user_id'; Context::set(CONTEXT_USER_ID, $id);
协程上下文不是“用了就安全”,而是“用对了才安全”。key 冲突、透传遗漏、生命周期误配,三个点任何一个出问题,数据错乱就会重现。别指望调试时靠猜,上线前就该把 Context 的 key 设计和透传路径画清楚。











