Hyperf连接池详解

本文详细解析了Hyperf框架中的连接池机制,包括为什么要使用连接池,连接池的工作原理,以及如何自定义连接池。通过官方文档的案例,解释了连接池如何提升服务性能,减少资源开销。同时,文章通过分析Hyperf自带的DbPool源码,展示了连接池的配置选项和使用方法,并给出了实际的压测经验,帮助读者更好地理解和应用连接池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开头语

 

Hyperf官网介绍了为什么要使用连接池及连接池的简单案例,但是对新手却不太友好,不过我还是想基于官方文档,来做一篇更深的讲解。

先来看看官方文档对于连接池的介绍

为什么要使用连接池

当并发量很低的时候,连接可以临时建立,但当服务吞吐达到几百、几千的时候,频繁 建立连接 Connect销毁连接 Close 就有可能会成为服务的一个瓶颈,那么当服务启动的时候,先建立好若干个连接并存放于一个队列中,当需要使用时从队列中取出一个并使用,使用完后再反还到队列去,而对这个队列数据结构进行维护的,就是连接池。

简单来说就是要提升性能,减少没必要的开销,提前建立好数据库的链接并放到池子里,后边再用的时候直接从里边取就好了。

传统模式请求示意图:

连接池模式请求示意图:

 连接池模式我觉得可以理解为设计模式中的注册树模式,注册树模式是项目启动的时候把实例化的类放到数组里,随用随取,而连接池模式是把数据库连接放到连接池里,随用随取,只不过两者不同的是连接池有数量限制。

官方给的自定义连接池简单案例

定义一个连接池首先需要实现一个继承了 Hyperf\Pool\Pool 的子类并实现抽象方法 createConnection,并返回一个实现了 Hyperf\Contract\ConnectionInterface 接口的对象,这样您创建的连接池对象就已经完成了,如下示例:

<?php
namespace App\Pool;

use Hyperf\Contract\ConnectionInterface;
use Hyperf\Pool\Pool;

class MyConnectionPool extends Pool
{
    public function createConnection(): ConnectionInterface
    {
        return new MyConnection();
    }
}

 这样便可以通过对实例化后的 MyConnectionPool 对象调用 get(): ConnectionInterface 和 release(ConnectionInterface $connection): void 方法执行连接的取用和归还了。

再看看看连接池定义后的简单使用:

<?php

use Hyperf\Pool\SimplePool\PoolFactory;
use Swoole\Coroutine\Http\Client;

$factory = $container->get(PoolFactory::class);

$pool = $factory->get('your pool name', function () use ($host, $port, $ssl) {
    return new Client($host, $port, $ssl);
}, [
    'max_connections' => 50
]);

$connection = $pool->get();

$client = $connection->getConnection(); // 即上述 Client.

// Do something.

$connection->release();

 这里我忍不住想吐槽一下,没错,把两个官方提供的 简单案例看完,更懵了。所以为了解惑,我便看了一下Hyperf框架自带的数据库Db连接池DbPool的源码。

咱们现在跟着Hyperf的源码看一下:

首先来看数据库的配置文件databases.php:

return [
    'default' => [
        'driver' => env('DB_DRIVER', 'mysql'),
        'host' => env('DB_HOST', '172.28.48.1'),
        'port' => env('DB_PORT', 3306),
        'database' => env('DB_DATABASE', 'seckill'),
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', '123456'),
        'charset' => env('DB_CHARSET', 'utf8mb4'),
        'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
        'prefix' => env('DB_PREFIX', ''),
        //上边几项关于数据库的基本配置咱们就不提了,主要看连接池的配置
        'pool' => [
            'min_connections' => 1,//最小连接数
            'max_connections' => 50,//最大连接数
            'connect_timeout' => 10.0,//连接超时时间
            'wait_timeout' => 10.0,//等待时间,这个框架默认的是3秒,我进行压测的时候修改成了10秒
            'heartbeat' => -1,
            'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
        ],
        ......
    ],
];

配置文件中关于连接池的配置我已经注释了出来,我希望你能熟悉这些配置,后边会用到,咱们现在来看看关于框架自带的关于DbPool连接池的源码,注意我在源码中的注释,这很重要

<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
namespace Hyperf\DbConnection\Pool;

use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\DbConnection\Connection;
use Hyperf\DbConnection\Frequency;
use Hyperf\Pool\Pool;
use Hyperf\Utils\Arr;
use Psr\Container\ContainerInterface;

//继承了Pool类
class DbPool extends Pool
{
    protected $name;//可以理解为配置文件中的键值

    protected $config;

    public function __construct(ContainerInterface $container, string $name)
    {
        $this->name = $name;
        //获取config实例
        $config = $container->get(ConfigInterface::class);
        //从这里大概能看出来,$key应该是databases.php配置文件中的某个key
        $key = sprintf('databases.%s', $this->name);
        //如果这个key不存在,会抛出异常
        if (! $config->has($key)) {
            throw new \InvalidArgumentException(sprintf('config[%s] is not exist!', $key));
        }
        // Rewrite the `name` of the configuration item to ensure that the model query builder gets the right connection.
        //重写配置项的“name”,以确保模型查询生成器获得正确的连接
        $config->set("{$key}.name", $name);

        //获取配置
        $this->config = $config->get($key);
        //从配置中提取关于pool的配置,也就是咱们在刚刚的配置文件databases.php中重点提到的
        $options = Arr::get($this->config, 'pool', []);
        //这行可以忽略
        $this->frequency = make(Frequency::class, [$this]);
        //调用了父类Pool的构造方法
        parent::__construct($container, $options);
    }

    public function getName(): string
    {
        return $this->name;
    }

    //创建连接
    protected function createConnection(): ConnectionInterface
    {
        return new Connection($this->container, $this, $this->config);
    }
}

还记不记得前边官方文档提到的自定义连接池的使用,忘记的话可以回到顶部看一下,我在这里再简单提一下,自定义连接池MyConnectionPool 需要继承Pool类,这实例化的 MyConnectionPool 对象通过调用 get():ConnectionInterface 和 release(ConnectionInterface $connection): void 方法就可以执行连接的取用和归还了。而这两个方法就来自于父类Pool。

现在就来看下Pool类的这两个方法:

 public function get(): ConnectionInterface
    {
        $connection = $this->getConnection();

        try {
            if ($this->frequency instanceof FrequencyInterface) {
                $this->frequency->hit();
            }

            if ($this->frequency instanceof LowFrequencyInterface) {
                if ($this->frequency->isLowFrequency()) {
                    $this->flush();
                }
            }
        } catch (\Throwable $exception) {
            if ($this->container->has(StdoutLoggerInterface::class) && $logger = $this->container->get(StdoutLoggerInterface::class)) {
                $logger->error((string) $exception);
            }
        }

        return $connection;
    }

    public function release(ConnectionInterface $connection): void
    {
        $this->channel->push($connection);
    }

release方法很简单,释放连接。主要讲解一下get方法。

get方法主要调用了getConnection方法,来看下getConnection源码,注意看注释:

private function getConnection(): ConnectionInterface
    {
        //获取channel的长度
        $num = $this->getConnectionsInChannel();

        try {
            //如果channel长度为0并且连接数小于最大连接数,则执行连接数+1,创建一个新的连接
            if ($num === 0 && $this->currentConnections < $this->option->getMaxConnections()) {
                ++$this->currentConnections;
                return $this->createConnection();
            }
        } catch (Throwable $throwable) {
            //如果有异常,连接数-1
            --$this->currentConnections;
            throw $throwable;
        }

        //$this->option->getWaitTimeout()获取的是配置文件中的
          wait_timeout(等待时间),如果连接池满了,并且在等待时
          间内没有获取到连接,就会抛出异常
        $connection = $this->channel->pop($this->option->getWaitTimeout());
        if (! $connection instanceof ConnectionInterface) {
            throw new RuntimeException('Connection pool exhausted. Cannot establish new connection before wait_timeout.');
        }
        return $connection;
    }

看到了get方法的最后,你也许能明白我进行压测的时候,要把wait_timeout改为10s的原因。我按照上边配置文件中的配置,用jmeter进行了10000并发的压测,就会抛出“Connection pool exhausted. Cannot establish new connection before wait_timeout.”这个异常。

现在咱们来看下DbPool的使用,为了不动源码,我新建了一个自定义的TestPool,继承DbPool,然后通过单例模式进行对DbPool 的调用:

<?php
namespace App\Pool;

use Hyperf\DbConnection\Pool\DbPool;
use phpDocumentor\Reflection\Types\Self_;
use Psr\Container\ContainerInterface;

class TestPool extends DbPool{
    private static $_instance;

    public function __construct(ContainerInterface $container, string $name)
    {
        parent::__construct($container, $name);
    }

    static function getInstance(ContainerInterface $container,string $name){
        if (!self::$_instance){
            self::$_instance = new self($container,$name);
        }

        return self::$_instance;
    }
}

然后在控制器中调用TestPool:

$TestPool = TestPool::getInstance($this->container, 'default');
echo $TestPool->getCurrentConnections() . "\n";//获取当前连接数
echo "channel:".$TestPool->getConnectionsInChannel() . "\n";//获取channel长度
try {
     $connection = $TestPool->get();

     $client = $connection->getConnection();
     $res = $client->table('wolive_visiter')->limit(10)->get();
     $connection->release();
     return ['res_count' => count($res)];
} catch (Exception $exception) {
     throwException($exception);
}

Hyperf中连接池的使用大概就是这样了

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

对这是我的昵称

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值