单例不能存用户信息,因hyperf中单例是进程级的,而用户身份是请求级的,必须用context隔离;正确做法是在中间件中context::set('current_user'),业务类通过context::get安全读取。

Hyperf 中直接用单例存用户信息,必然串号。这不是 bug,是协程共享内存的必然结果——你写的 private static $user 在整个 Worker 进程里就一份,A 请求写进去,B 请求进来直接读到 A 的数据。
为什么单例不能存当前用户
Hyperf 运行在 Swoole 常驻内存模型下,$container->get(UserService::class) 返回的是进程级单例,生命周期和 Worker 进程一致。而用户身份是请求级状态,必须隔离。常见错误现象包括:
- 登录用户 A,刷新页面看到用户 B 的头像或订单
- JWT 解析后把
$user赋给单例属性,后续所有请求都复用该对象 - 用
Context::set('user', $user)但没配对使用Context::get('user'),或在非协程上下文(如定时任务)里误读
用 Context 实现请求级“伪单例”
不是放弃单例,而是把“有状态部分”从单例里剥出来,交给 Hyperf\Utils\Context 管理。它底层基于 Swoole 的协程上下文,每个请求独享一份。
正确做法:
- 单例类(如
UserService)只保留无状态逻辑:校验 token、查库、组装数据 - 用户实体本身不存为类属性,每次需要时调用
Context::get('current_user') - 中间件中统一注入:
Context::set('current_user', $user),且确保该中间件在路由前执行 - 避免在构造函数里读
Context—— 构造发生在容器初始化阶段,此时协程上下文尚未建立
示例:
class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $request->getHeaderLine('Authorization');
$user = JwtService::verify($token);
if ($user) {
Context::set('current_user', $user); // ✅ 此处协程已就位
}
return $handler->handle($request);
}
}
make() 创建短生命周期对象的适用边界
有人想绕过单例,改用 make(UserService::class) 每次新建。这能解决串号,但代价高:
-
make()只让目标类本身短周期,其依赖(如PDO、Redis)仍是单例 —— 这是合理设计,别动 - 如果
UserService自身不带状态,make()和get()效果几乎一样,纯属浪费对象创建开销 - 真正该短周期的,是携带了请求上下文的包装器(如
CurrentUserService),而非业务服务本身
更轻量的替代方案:
class CurrentUserService
{
public function getUser(): ?User
{
return Context::get('current_user');
}
}
这个类可以注册为单例(它自己无状态),只负责安全地读取上下文。
容易被忽略的 Context 生命周期陷阱
Context 不是万能胶。它的 key 是协程局部的,但如果你在协程外(比如 go(function () { ... }) 外部)、或在子协程里没正确传递,就会读不到。尤其注意:
- 异步任务(
go())默认不继承父协程的Context,需手动Context::copy() - 定时任务(
@Cron)运行在独立协程,没有 HTTP 上下文,Context::get('current_user')必然为null -
Context::set()后未检查返回值,无法确认是否成功(建议加日志或断言)











