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% 的请求级资源管理场景。

核心联动思路(通用):

  1. 用 Di 容器绑定请求级资源的实现类,设置为多例(Prototype),确保每个请求能获取到独立实例;

  2. 在请求入口(如中间件),通过 Di 容器获取资源实例,存入协程上下文;

  3. 主协程、子协程通过协程上下文获取资源实例,无需手动传递;

  4. 请求结束(协程终止),协程上下文自动销毁,资源实例随之释放(若需手动释放,可在中间件后置处理中操作)。

场景 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 生成)

更多推荐