开头语
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中连接池的使用大概就是这样了