hyperf 3.1 中协程上下文丢失会导致 context::get() 返回 null、依赖注入失败、db 连接不可用等,接口偶发 500 或数据错乱,日志缺失请求 id 关联痕迹;根本原因是协程切换或跨进程时上下文被清空或未正确绑定。

Hyperf 3.1 中协程上下文丢失会导致 Context::get() 返回 null、依赖注入失败、Db 连接不可用等现象,接口偶发 500 或数据错乱,日志里找不到请求 ID 关联痕迹——这不是代码写错了,而是上下文在协程切换或跨进程时被清空或未正确绑定。
确认是否真丢了上下文
第一步:在控制器入口打点,验证当前协程是否有 request 实例
执行 var_dump(Context::has(ServerRequestInterface::class));,返回 false 即已丢失;若为 true 但后续某处变 false,说明在中间件、AOP 或异步调用中被覆盖或清除。
第二步:检查是否误用了 go() 启动裸协程
【裸协程不继承父协程上下文】,例如 go(function () { Context::get(...) }); 必然失败。必须改用 Co::create() 或 Coroutine::create()(Hyperf 封装版),它们会自动拷贝当前上下文。
第三步:查看是否在 TaskWorker 中尝试取 Context
TaskWorker 是独立进程,Context::get() 在 onTask 回调里永远为 null——这不是 Bug,是设计使然。需确认业务逻辑是否错误地把上下文敏感操作投递到了 task。
排查中间件/注解导致的上下文覆盖
方法一:禁用所有自定义中间件,只留 Hyperf 默认中间件链
临时注释 config/autoload/middlewares.php 中除 Hyperf\HttpServer\Middleware\ServerMiddleware 外的所有项,重启服务再测。若恢复正常,问题出在某个中间件里手动调用了 Context::set() 且未 restore。
方法二:检查 AOP 切面是否重置了上下文
重点审查带有 @Aspect 注解的类,尤其是 around 方法里是否写了 Context::set(...) 但没配 finally 块还原。常见坑:在切面中 new 了一个新 Request 对象并 set 进去,却忘了把原始对象存档和回填。
方法三:确认注解解析器是否触发了上下文重建
如果用了 @Inject、@Value 等注解,且容器在非标准时机(如定时任务 BootManager 中)提前初始化,可能导致 Context 初始化晚于依赖注入时机。此时 Container::get() 可能返回空实例,而非抛异常。
定位跨协程传递失效点
① 查看是否在协程内启动了子协程但未传递上下文
比如:go(function () use ($request) { $user = UserService::find($request->getAttribute('uid')); }); —— 这里 $request 是变量引用,但 $request 内部可能持有不可序列化资源(如 uploaded file stream),实际传过去的是空壳。应显式提取所需字段:$uid = $request->getAttribute('uid'); → go(function () use ($uid) { ... });
② 检查 Redis pipeline/multi 是否中途异常退出
若 $redis->multi() 后抛异常且没进 finally,连接上下文不会释放,后续该协程内所有 Context::get() 都可能返回旧值或 null。加日志:var_dump(Context::has($redis->getContextKey())); 在 multi 前后各打一次,若前 true 后 false,基本锁定泄漏点。
③ 验证 Swoole 定时器回调是否运行在干净协程中Swoole\Timer::after(1000, function () { var_dump(Context::get(...)); }); —— 此回调默认不带任何上下文。必须手动绑定:Swoole\Timer::after(1000, function () { $ctx = Context::copy(); Context::set($ctx); ... });,否则 Context::get() 必然为空。
快速验证上下文生命周期
在 App\Kernel\Event\BootApplication 中注册监听器,打印协程启动时的上下文快照:Context::set('boot_time', microtime(true));Context::set('coro_id', Coroutine::id());
然后在任意控制器中输出这两项,若 coro_id 匹配但 boot_time 为空,说明中间某处调用了 Context::clear() 或覆盖了 key。
注意:【Context::clear() 是全局清空,不可逆】,一旦执行,当前协程所有上下文数据永久丢失,且无法通过任何方式恢复。











