Hyperf 协程上下文与 Di 容器联动:优雅管理请求级资源
Hyperf 协程上下文与 Di 容器联动:优雅管理请求级资源
一、开篇引入:请求级资源管理的“痛点困局”
1. 痛点直击:你是否也遇到过这些资源管理难题?
在上一篇《Hyperf 协程上下文详解》中,我们解决了多协程数据共享错乱的问题,但在实际 Hyperf 开发中,除了“数据共享”,请求级资源管理 更是高频痛点——尤其是在高并发接口场景下,每个请求都需要独立的资源实例(如数据库连接、Redis 客户端、自定义服务实例),若管理不当,会导致资源污染、内存泄漏、并发性能下降等一系列问题。
举几个实战中最常见的场景,看看你是否踩过坑:
-
用 Di 容器绑定单例服务,多个请求的协程共用同一个资源实例,导致请求间数据串流(如 A 请求的数据库查询影响 B 请求的结果);
-
每次请求都手动创建资源实例(如 new Redis()、new Db()),代码冗余且无法复用,还容易遗漏资源释放,引发内存泄漏;
-
子协程无法优雅获取请求级资源实例,只能通过参数手动传递,代码可读性差、维护成本高;
-
协程上下文与 Di 容器单独使用,资源实例与请求上下文脱节,无法实现“请求结束自动释放资源”。
很多开发者误以为“协程上下文管数据,Di 容器管服务”,两者是独立的,却忽略了它们的联动价值——实际上,将协程上下文的“请求级隔离”与 Di 容器的“依赖注入、实例管理”结合,才能实现请求级资源的优雅管理,既保证资源隔离,又兼顾代码简洁性和可维护性。
2. 本文价值:手把手教你联动实战,彻底解决资源管理难题
本文全程聚焦“协程上下文 + Di 容器”的联动用法,不堆砌底层原理,不搞晦涩概念,从“为什么联动→联动核心逻辑→实战用法→避坑指南”,一步步教你优雅管理请求级资源。
无论你是刚接触 Hyperf 依赖注入的新手,还是已经熟练使用协程上下文、但资源管理混乱的中级开发者,读完本文都能收获:
-
理解协程上下文与 Di 容器联动的核心逻辑,明白“为什么要联动”“联动能解决什么问题”;
-
掌握 3 个核心联动场景(请求级数据库连接、Redis 客户端、自定义服务),代码可直接复制套用;
-
学会优雅管理请求级资源,避免资源污染、内存泄漏,提升项目并发性能和可维护性;
-
避开联动过程中的常见坑,减少调试时间,规范代码开发。
3. 开篇小结:联动的核心意义
Hyperf 的 Di 容器(依赖注入容器)核心作用是“管理服务实例、实现依赖解耦”,支持单例、多例等不同生命周期的实例绑定;协程上下文的核心作用是“实现请求级数据隔离,支持父子协程数据传递”。
两者联动的核心意义的是:用 Di 容器创建和管理资源实例,用协程上下文存储和传递请求级的实例,实现“资源实例复用 + 请求级隔离 + 自动释放”,彻底解决请求级资源管理的混乱问题,让代码更优雅、更稳定。
二、基础铺垫:必懂的核心概念(小白也能秒懂)
在开始联动实战之前,我们先快速回顾两个核心组件的关键知识点,重点明确“为什么单独使用会有问题”,为后续联动用法做好铺垫——避免因概念混淆导致踩坑。
1. 快速回顾:协程上下文核心特性
协程上下文(Hyperf\Coroutine\Context)的核心特性的是“隔离性”和“传递性”,重点记住 2 点:
-
隔离性:每个请求对应一个主协程,主协程及其子协程共享同一个上下文,不同请求的协程上下文完全隔离,数据互不干扰;
-
临时性:上下文数据与协程生命周期绑定,请求结束(协程终止)后,上下文数据自动销毁,无需手动清理。
核心局限:协程上下文仅负责“存储和传递数据/实例”,不负责实例的创建、复用和释放,单独使用无法实现资源实例的高效管理。
2. 快速回顾:Di 容器核心特性
Di 容器(Hyperf\Di\Container)的核心特性是“依赖注入、实例管理”,重点记住 2 个关键用法(结合实战高频场景):
-
实例绑定:通过 bind() 方法绑定接口与实现类,支持单例(Singleton)、多例(Prototype)两种生命周期——单例全局共享一个实例,多例每次获取都创建新实例;
-
实例获取:通过 get() 方法获取绑定的实例,或通过 @Inject 注解实现自动注入,无需手动 new 实例,实现依赖解耦。
核心局限:Di 容器的单例绑定会导致多请求协程共用实例(资源污染),多例绑定每次都创建新实例(资源浪费),单独使用无法适配请求级资源的“隔离+复用”需求。Hyperf 中 Di 容器在服务启动时初始化,常驻内存,无需每次请求重建,这一特性也决定了单例绑定不适合请求级资源。
3. 关键结论:为什么必须联动?
单独使用协程上下文或 Di 容器,都无法完美解决请求级资源管理问题,两者联动才能互补:
-
Di 容器负责“创建资源实例”,通过多例绑定保证每个请求能获取到独立实例,通过依赖注入实现实例复用,避免手动 new 实例的冗余;
-
协程上下文负责“存储和传递实例”,通过请求级隔离保证实例不被其他请求污染,通过父子协程传递让子协程无需手动传递实例,同时利用上下文的临时性实现资源自动释放。
简单来说:Di 容器管“实例的创建和复用”,协程上下文管“实例的隔离和传递”,两者结合,才能实现请求级资源的优雅管理。这里需要注意,避免在构造函数或 Di 配置中直接调用容器获取实例,以免因协程上下文未就绪导致空调用崩溃,应改用注解注入或运行时获取实例。
三、核心实战:协程上下文与 Di 容器联动的 3 个高频场景
这部分是本文的重点,也是实战中最常用的内容。每个场景都遵循“场景描述→联动思路→代码示例→关键说明”的结构,聚焦“落地性”,所有代码示例均可直接复制到项目中使用,覆盖 80% 的请求级资源管理场景。
核心联动思路(通用):
-
用 Di 容器绑定请求级资源的实现类,设置为多例(Prototype),确保每个请求能获取到独立实例;
-
在请求入口(如中间件),通过 Di 容器获取资源实例,存入协程上下文;
-
主协程、子协程通过协程上下文获取资源实例,无需手动传递;
-
请求结束(协程终止),协程上下文自动销毁,资源实例随之释放(若需手动释放,可在中间件后置处理中操作)。
场景 1:请求级数据库连接管理(最常用)
1. 场景描述
高并发接口中,每个请求需要独立的数据库连接(避免多请求共用连接导致的事务错乱、数据串流),同时需要复用连接实例(避免每次请求都创建新连接,浪费资源),还要支持子协程获取连接实例,用于并发处理数据库操作。
2. 联动思路
-
Di 容器绑定数据库连接类(或自定义 DbService),设置为多例,确保每个请求获取独立连接;
-
中间件中,通过 Di 容器获取数据库连接实例,存入协程上下文;
-
控制器、子协程从协程上下文获取连接实例,执行数据库操作;
-
请求结束,协程上下文销毁,连接实例自动释放(Hyperf 数据库组件会自动回收连接到连接池)。
3. 代码示例(可直接复制使用)
步骤 1:Di 容器绑定数据库服务(多例)
在 config/autoload/di.php 中绑定自定义数据库服务,设置为多例(Prototype),避免使用闭包直接返回实例,改用工厂函数确保协程安全:
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSourceFactory;
use App\Service\DbService;
return [
// 绑定自定义数据库服务,设置为多例
DbService::class => function (Container $container) {
// 从容器获取数据库连接配置
$config = $container->get(\Hyperf\Contract\ConfigInterface::class)->get('databases.default');
// 返回新的数据库服务实例(多例)
return new DbService($config);
},
// 关键:设置为多例,每次get()都创建新实例
'di' => [
'definitions' => DefinitionSourceFactory::build([
DbService::class => [
'scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE,
],
]),
],
];
步骤 2:自定义数据库服务(DbService)
namespace App\Service;
use Hyperf\Database\Connection;
use Hyperf\Database\ConnectionFactory;
class DbService
{
/**
* @var Connection 数据库连接实例
*/
private $connection;
/**
* 构造函数:初始化数据库连接
*/
public function __construct(array $config)
{
$factory = new ConnectionFactory();
$this->connection = $factory->make($config);
}
/**
* 获取数据库连接实例
*/
public function getConnection(): Connection
{
return $this->connection;
}
/**
* 示例:执行查询操作
*/
public function query(string $sql, array $bindings = []): array
{
return $this->connection->select($sql, $bindings);
}
/**
* 示例:执行事务操作
*/
public function transaction(\Closure $closure)
{
return $this->connection->transaction($closure);
}
}
步骤 3:中间件中存入协程上下文
namespace App\Middleware;
use Hyperf\Coroutine\Context;
use Hyperf\Di\Container;
use App\Service\DbService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class DbContextMiddleware implements MiddlewareInterface
{
public function __construct(protected Container $container)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 1. 通过Di容器获取数据库服务实例(多例,每个请求一个新实例)
$dbService = $this->container->get(DbService::class);
// 2. 将实例存入协程上下文,供后续协程使用
Context::set('db_service', $dbService);
// 3. 执行后续中间件和控制器
$response = $handler->handle($request);
// (可选)请求结束后,手动释放连接(Hyperf会自动回收,可省略)
// $dbService->getConnection()->disconnect();
return $response;
}
}
步骤 4:控制器/子协程中获取使用
namespace App\Controller;
use Hyperf\Coroutine\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\HttpServer\Contract\RequestInterface;
use App\Service\DbService;
class UserController
{
public function getUserList(RequestInterface $request)
{
// 1. 主协程从协程上下文获取数据库服务实例
/** @var DbService $dbService */
$dbService = Context::get('db_service');
// 2. 主协程执行数据库查询
$users = $dbService->query('SELECT * FROM users LIMIT 10');
// 3. 创建子协程,并发查询用户详情(子协程自动继承上下文实例)
Coroutine::create(function () {
/** @var DbService $dbService */
$dbService = Context::get('db_service');
// 子协程执行事务操作,使用独立的数据库连接
$dbService->transaction(function () use ($dbService) {
$dbService->query('UPDATE users SET status = 1 WHERE id = ?', [1]);
});
});
return [
'code' => 200,
'message' => 'success',
'data' => $users
];
}
}
4. 关键说明
-
Di 容器绑定 DbService 时,必须设置为多例(ScopeEnum::PROTOTYPE),否则所有请求会共用一个连接实例,导致数据串流;
-
中间件是请求入口,在这里获取实例并存入上下文,确保后续所有协程(主协程、子协程)都能获取到;
-
Hyperf 数据库组件会自动管理连接池,请求结束后连接会自动回收,无需手动释放,若有特殊需求(如自定义连接),可在中间件后置处理中手动释放。
场景 2:请求级 Redis 客户端管理(高频)
1. 场景描述
接口请求中,需要使用 Redis 存储临时数据(如请求缓存、验证码),每个请求需要独立的 Redis 客户端实例(避免多请求共用客户端导致的命令错乱),同时需要支持子协程获取客户端,实现并发缓存操作。
2. 联动思路
-
Di 容器绑定 Redis 客户端实例,设置为多例,确保每个请求获取独立客户端;
-
中间件中,通过 Di 容器获取 Redis 客户端,存入协程上下文;
-
控制器、子协程从上下文获取客户端,执行 Redis 操作;
-
请求结束,上下文销毁,客户端实例自动释放,连接归还到 Redis 连接池。
3. 代码示例(可直接复制使用)
步骤 1:Di 容器绑定 Redis 客户端(多例)
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSourceFactory;
use Hyperf\Redis\Redis;
return [
// 绑定Redis客户端,设置为多例
Redis::class => function (Container $container) {
$config = $container->get(\Hyperf\Contract\ConfigInterface::class)->get('redis.default');
// 创建Redis客户端实例(多例)
return make(Redis::class, ['config' => $config]);
},
// 设置为多例
'di' => [
'definitions' => DefinitionSourceFactory::build([
Redis::class => [
'scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE,
],
]),
],
];
步骤 2:中间件中存入协程上下文
namespace App\Middleware;
use Hyperf\Coroutine\Context;
use Hyperf\Di\Container;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RedisContextMiddleware implements MiddlewareInterface
{
public function __construct(protected Container $container)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 1. 通过Di容器获取Redis客户端(多例,每个请求一个新实例)
$redis = $this->container->get(Redis::class);
// 2. 存入协程上下文
Context::set('redis_client', $redis);
// 3. 执行后续逻辑
$response = $handler->handle($request);
// (可选)手动释放连接
// $redis->disconnect();
return $response;
}
}
步骤 3:控制器/子协程中使用
namespace App\Controller;
use Hyperf\Coroutine\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Redis\Redis;
class CacheController
{
public function setCache(RequestInterface $request)
{
$key = $request->input('key');
$value = $request->input('value');
// 1. 主协程获取Redis客户端
/** @var Redis $redis */
$redis = Context::get('redis_client');
// 执行缓存设置
$redis->set($key, $value, 3600);
// 2. 子协程并发获取缓存(继承上下文实例)
Coroutine::create(function () use ($key) {
/** @var Redis $redis */
$redis = Context::get('redis_client');
$cacheValue = $redis->get($key);
echo "子协程获取缓存:{$key} = {$cacheValue}\n";
});
return [
'code' => 200,
'message' => '缓存设置成功',
'data' => ['key' => $key, 'value' => $value]
];
}
}
4. 关键说明
-
Redis 客户端绑定为多例,避免多请求共用客户端导致的命令串流(如 A 请求的 set 操作覆盖 B 请求的 set 操作);
-
Hyperf\Redis\Redis 组件已集成连接池,请求结束后客户端会自动归还连接,无需手动释放;
-
若需要使用多个 Redis 连接池(如业务缓存、会话缓存),可在 Di 容器中绑定多个实例,存入上下文时用不同的 key 区分(如 ‘redis_business’、‘redis_session’)。
场景 3:请求级自定义服务实例管理(实战必备)
1. 场景描述
实战中,我们会自定义很多业务服务(如用户权限服务、日志服务),这些服务需要依赖请求级数据(如用户 ID、请求 ID),每个请求需要独立的服务实例(避免多请求数据污染),同时需要支持子协程获取服务实例,实现业务逻辑复用。
2. 联动思路
-
Di 容器绑定自定义业务服务,设置为多例,注入请求级依赖(如用户 ID、请求 ID);
-
中间件中,先获取请求级数据(如用户 ID),通过 Di 容器创建服务实例(注入依赖),存入协程上下文;
-
控制器、子协程从上下文获取服务实例,调用业务方法;
-
请求结束,上下文销毁,服务实例自动释放,避免内存泄漏。
3. 代码示例(可直接复制使用)
步骤 1:自定义业务服务(用户权限服务)
namespace App\Service;
use Hyperf\Coroutine\Context;
class UserAuthService
{
/**
* @var int 用户ID(请求级依赖)
*/
private $userId;
/**
* @var string 请求ID(请求级依赖)
*/
private $requestId;
/**
* 构造函数:注入请求级依赖
*/
public function __construct(int $userId, string $requestId)
{
$this->userId = $userId;
$this->requestId = $requestId;
}
/**
* 示例:校验用户权限
*/
public function checkPermission(string $permission): bool
{
// 模拟权限校验逻辑,依赖当前请求的用户ID
echo "请求ID:{$this->requestId},用户ID:{$this->userId},校验权限:{$permission}\n";
// 实际业务中,可查询数据库/Redis校验权限
return true;
}
/**
* 示例:获取用户信息(依赖用户ID)
*/
public function getUserInfo(): array
{
return [
'user_id' => $this->userId,
'request_id' => $this->requestId,
'name' => '测试用户',
'permission' => ['read', 'write']
];
}
}
步骤 2:Di 容器绑定自定义服务(多例)
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSourceFactory;
use App\Service\UserAuthService;
return [
// 绑定用户权限服务,设置为多例,动态注入依赖
UserAuthService::class => function (Container $container) {
// 从协程上下文获取请求级依赖(需在中间件中先存入)
$userId = Context::get('user_id', 0);
$requestId = Context::get('request_id', uniqid());
// 注入依赖,返回新实例(多例)
return new UserAuthService($userId, $requestId);
},
// 设置为多例
'di' => [
'definitions' => DefinitionSourceFactory::build([
UserAuthService::class => [
'scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE,
],
]),
],
];
步骤 3:中间件中存入请求级数据和服务实例
namespace App\Middleware;
use Hyperf\Coroutine\Context;
use Hyperf\Di\Container;
use App\Service\UserAuthService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class UserAuthContextMiddleware implements MiddlewareInterface
{
public function __construct(protected Container $container)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 1. 获取请求级数据(用户ID、请求ID)
$requestId = $request->header('X-Request-Id', uniqid());
$userId = (int)$request->getHeaderLine('X-User-Id', 0);
// 2. 存入协程上下文(供服务注入使用)
Context::set('request_id', $requestId);
Context::set('user_id', $userId);
// 3. 通过Di容器获取用户权限服务实例(多例,注入依赖)
$userAuthService = $this->container->get(UserAuthService::class);
// 4. 将服务实例存入协程上下文
Context::set('user_auth_service', $userAuthService);
// 5. 执行后续逻辑
$response = $handler->handle($request);
return $response;
}
}
步骤 4:控制器/子协程中使用
namespace App\Controller;
use Hyperf\Coroutine\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\HttpServer\Contract\RequestInterface;
use App\Service\UserAuthService;
class AuthController
{
public function check(RequestInterface $request)
{
$permission = $request->input('permission', 'read');
// 1. 主协程获取用户权限服务实例
/** @var UserAuthService $authService */
$authService = Context::get('user_auth_service');
// 2. 调用服务方法,校验权限
$hasPermission = $authService->checkPermission($permission);
$userInfo = $authService->getUserInfo();
// 3. 子协程获取服务实例,复用业务逻辑
Coroutine::create(function () use ($permission) {
/** @var UserAuthService $authService */
$authService = Context::get('user_auth_service');
$authService->checkPermission($permission);
});
return [
'code' => 200,
'message' => $hasPermission ? '权限校验通过' : '权限不足',
'data' => [
'user_info' => $userInfo,
'permission' => $permission,
'has_permission' => $hasPermission
]
];
}
}
4. 关键说明
-
自定义服务的依赖(如用户 ID、请求 ID)需先存入协程上下文,再通过 Di 容器的工厂函数注入,确保每个请求的服务实例都有独立的依赖数据;
-
服务绑定为多例,避免多请求共用服务实例,导致依赖数据污染(如 A 请求的用户 ID 被 B 请求覆盖);
-
这种联动方式可复用业务逻辑,减少代码冗余,同时保证请求级隔离,适合所有需要依赖请求级数据的自定义服务。需要注意,避免在服务构造函数中执行耗时操作或调用协程相关方法,以免引发上下文异常。
四、核心技巧:联动避坑指南(少走弯路,必看)
结合实战经验,总结 5 个联动过程中的高频坑,每个坑配“问题现象→核心原因→解决方案”,帮你避免调试踩坑,确保资源管理优雅、稳定。
坑 1:Di 容器绑定为单例,导致请求间资源污染
1. 问题现象
多请求并发时,A 请求的资源实例(如数据库连接、服务实例)被 B 请求篡改,导致数据串流、权限校验错误等问题。这是新手最容易踩的坑,误用 Laravel 思维将服务绑定为单例,忽略 Hyperf 服务常驻的特性,导致全局共享实例引发污染。
2. 核心原因
Di 容器默认绑定为单例(Singleton),全局共享一个实例,多个请求的协程共用同一个资源实例,导致请求间数据污染。
3. 解决方案
所有请求级资源(数据库连接、Redis 客户端、自定义业务服务),在 Di 容器绑定的时,必须设置为多例(ScopeEnum::PROTOTYPE),确保每个请求获取独立的实例。
// 正确做法:设置为多例
'redefinitions' => [
App\Service\DbService::class => [
'scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE,
],
],
// 错误做法:默认单例(全局共享)
'redefinitions' => [
App\Service\DbService::class => [], // 未设置scope,默认Singleton
],
坑 2:协程上下文未就绪时,Di 容器工厂函数获取上下文数据
1. 问题现象
Di 容器工厂函数中调用 Context::get() 获取数据时,始终返回默认值,导致服务实例注入的依赖错误(如用户 ID 为 0)。
2. 核心原因
Di 容器工厂函数在服务启动时初始化,而协程上下文在请求到来时才会存入数据,工厂函数执行时,协程上下文尚未就绪,无法获取到请求级数据。此外,在构造函数或 Di 配置中直接调用容器获取实例,也可能因协程上下文未就绪导致空调用崩溃。
3. 解决方案
将“获取协程上下文数据”的操作,延迟到请求入口(中间件),先存入上下文数据,再通过 Di 容器获取服务实例;或在服务实例中提供 set 方法,在中间件中注入依赖。
// 正确做法:中间件中先存上下文,再获取服务实例
// 中间件中
Context::set('user_id', $userId);
$authService = $this->container->get(UserAuthService::class); // 此时可获取到user_id
// 或:服务中提供set方法,中间件中注入依赖
class UserAuthService
{
private $userId;
public function setUserId(int $userId)
{
$this->userId = $userId;
}
}
// 中间件中
$authService = $this->container->get(UserAuthService::class);
$authService->setUserId($userId);
Context::set('user_auth_service', $authService);
坑 3:手动创建资源实例,未通过 Di 容器和协程上下文管理
1. 问题现象
控制器、子协程中手动 new 资源实例(如 new Redis()、new DbService()),代码冗余,无法复用,且容易遗漏资源释放,引发内存泄漏。同时,手动创建的实例无法在子协程中优雅传递,只能通过参数传递,代码可读性差。
2. 核心原因
忽略 Di 容器的实例管理能力和协程上下文的传递能力,习惯手动创建实例,导致资源管理混乱。此外,手动创建实例可能会忽略协程安全,使用同步阻塞的资源连接,拖垮协程并发性能。
3. 解决方案
所有资源实例统一通过 Di 容器绑定和获取,存入协程上下文,主协程、子协程通过上下文获取,避免手动 new 实例。同时,确保资源实例是协程安全的,避免使用同步阻塞的资源连接。
坑 4:请求结束后,未释放资源导致内存泄漏
1. 问题现象
服务运行一段时间后,内存占用持续升高,最终导致服务崩溃,排查发现是请求级资源实例未释放,引发内存泄漏。
2. 核心原因
自定义资源实例(如非 Hyperf 内置的客户端),未在请求结束后手动释放,且协程上下文销毁时,实例未被自动回收(如存在循环引用)。
3. 解决方案
-
Hyperf 内置组件(数据库、Redis)会自动回收资源,无需手动释放;
-
自定义资源实例,在中间件后置处理中手动释放(如调用 disconnect() 方法);
-
避免资源实例存在循环引用(如服务实例引用上下文,上下文引用服务实例),确保实例能被正常回收。
坑 5:子协程手动创建资源实例,与主协程实例不一致
1. 问题现象
子协程中手动 new 资源实例,与主协程从上下文获取的实例不一致,导致数据同步问题(如主协程修改了缓存,子协程获取不到最新数据)。
2. 核心原因
子协程未从协程上下文获取实例,而是手动创建新实例,导致与主协程实例隔离,数据无法同步。
3. 解决方案
所有协程(主协程、子协程)统一从协程上下文获取资源实例,禁止手动创建,确保实例一致,数据同步。同时,避免在子协程中修改全局容器中的资源配置,防止影响其他协程。
五、实战案例:综合联动,优雅管理请求级资源
结合真实高并发场景,完整还原协程上下文与 Di 容器的联动流程,解决请求级资源管理难题,让你学会如何在实际项目中灵活运用联动技巧。
1. 场景描述
电商订单接口,QPS 300+,每个请求需要用到 3 种请求级资源:
-
数据库连接:用于查询订单数据、更新订单状态;
-
Redis 客户端:用于存储订单缓存、生成订单序列号;
-
订单服务实例:用于处理订单业务逻辑(依赖请求 ID、用户 ID)。
核心需求:所有资源实例独立(避免请求间污染),支持子协程并发使用,代码优雅、可维护,请求结束后资源自动释放,支撑高并发稳定运行。该场景类似每日优鲜秒杀系统、量化派金融风控等高频高并发场景的资源管理需求,对资源隔离和并发性能要求较高。
2. 落地步骤(可直接复制到项目中使用)
步骤 1:Di 容器绑定所有请求级资源(多例)
// config/autoload/di.php
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSourceFactory;
use App\Service\DbService;
use App\Service\RedisService;
use App\Service\OrderService;
return [
// 1. 数据库服务(多例)
DbService::class => function (Container $container) {
$config = $container->get(\Hyperf\Contract\ConfigInterface::class)->get('databases.default');
return new DbService($config);
},
// 2. Redis服务(多例)
RedisService::class => function (Container $container) {
$config = $container->get(\Hyperf\Contract\ConfigInterface::class)->get('redis.default');
return new RedisService($config);
},
// 3. 订单服务(多例,注入依赖)
OrderService::class => function (Container $container) {
$userId = \Hyperf\Coroutine\Context::get('user_id', 0);
$requestId = \Hyperf\Coroutine\Context::get('request_id', uniqid());
// 注入数据库服务和Redis服务(多例,自动获取独立实例)
$dbService = $container->get(DbService::class);
$redisService = $container->get(RedisService::class);
return new OrderService($userId, $requestId, $dbService, $redisService);
},
// 所有服务设置为多例
'di' => [
'definitions' => DefinitionSourceFactory::build([
DbService::class => ['scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE],
RedisService::class => ['scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE],
OrderService::class => ['scope' => \Hyperf\Di\ScopeEnum::PROTOTYPE],
]),
],
];
步骤 2:自定义资源服务(DbService、RedisService、OrderService)
此处省略 DbService 和 RedisService 的代码(参考前文场景 1、2),重点展示 OrderService 的实现:
namespace App\Service;
class OrderService
{
private $userId;
private $requestId;
private $dbService;
private $redisService;
public function __construct(int $userId, string $requestId, DbService $dbService, RedisService $redisService)
{
$this->userId = $userId;
$this->requestId = $requestId;
$this->dbService = $dbService;
$this->redisService = $redisService;
}
/**
* 创建订单(核心业务逻辑)
*/
public function createOrder(array $orderData): array
{
// 1. 生成订单序列号(使用Redis)
$orderNo = $this->generateOrderNo();
// 2. 数据库事务:创建订单
$orderId = $this->dbService->transaction(function () use ($orderData, $orderNo) {
return $this->dbService->query(
'INSERT INTO orders (order_no, user_id, amount, status) VALUES (?, ?, ?, ?)',
[$orderNo, $this->userId, $orderData['amount'], 1]
);
});
// 3. 缓存订单信息(使用Redis)
$this->redisService->set("order:{$orderNo}", json_encode(['order_id' => $orderId, 'user_id' => $this->userId]), 3600);
// 记录日志(关联请求ID)
echo "请求ID:{$this->requestId},用户ID:{$this->userId},创建订单:{$orderNo}\n";
return [
'order_id' => $orderId,
'order_no' => $orderNo,
'status' => 1
];
}
/**
* 生成订单序列号
*/
private function generateOrderNo(): string
{
$prefix = date('YmdHis');
$random = $this->redisService->incr('order_no_incr'); // 自增序列
return $prefix . str_pad($random, 6, '0', STR_PAD_LEFT);
}
}
步骤 3:中间件中存入请求数据和资源实例
namespace App\Middleware;
use Hyperf\Coroutine\Context;
use Hyperf\Di\Container;
use App\Service\DbService;
use App\Service\RedisService;
use App\Service\OrderService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ResourceContextMiddleware implements MiddlewareInterface
{
public function __construct(protected Container $container)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 1. 获取请求级数据
$requestId = $request->header('X-Request-Id', uniqid());
$userId = (int)$request->getHeaderLine('X-User-Id', 0);
// 2. 存入协程上下文
Context::set('request_id', $requestId);
Context::set('user_id', $userId);
// 3. 获取所有资源实例,存入协程上下文
$dbService = $this->container->get(DbService::class);
$redisService = $this->container->get(RedisService::class);
$orderService = $this->container->get(OrderService::class);
Context::set('db_service', $dbService);
Context::set('redis_service', $redisService);
Context::set('order_service', $orderService);
// 4. 执行后续逻辑
$response = $handler->handle($request);
// 5. (可选)释放自定义资源
// $dbService->getConnection()->disconnect();
// $redisService->disconnect();
return $response;
}
}
步骤 4:控制器中使用资源实例,创建子协程并发处理
namespace App\Controller;
use Hyperf\Coroutine\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\HttpServer\Contract\RequestInterface;
use App\Service\OrderService;
class OrderController
{
public function create(RequestInterface $request)
{
$orderData = $request->input('order_data');
// 1. 主协程获取订单服务实例
/** @var OrderService $orderService */
$orderService = Context::get('order_service');
// 2. 创建订单(核心业务)
$order = $orderService->createOrder($orderData);
// 3. 子协程1:并发查询订单详情(使用数据库服务)
Coroutine::create(function () use ($order) {
/** @var DbService $dbService */
$dbService = Context::get('db_service');
$orderDetail = $dbService->query('SELECT * FROM orders WHERE id = ?', [$
(注:文档部分内容可能由 AI 生成)
更多推荐
所有评论(0)