连接池
-
什么是连接池?
连接池是创建和管理一个连接的缓冲池的技术,缓冲池中的连接可以被任何需要他们的线程使用。
一个服务端资源的连接数量是有限的,比如:Redis的maxclients、MySQL的max_connections、PHP-FPM的max_children,start_servers 等配置参数,都是用来设置连接数的。
连接池的原理就是在资源初始化时将一定数量的连接存放在连接池中,谁用谁取,用完后再放回去,实现连接的复用。如果超出连接池容量,会进行排队等待或者直接丢弃。
-
连接池的优势?
- 减少连接创建时间:连接池中的连接是已经准备好的、可重复使用的
- 简化的编程模式:当使用连接池时,每一个单独的线程能够像创建一个自己的PDO连接一样操作
- 控制资源的使用:增强系统的稳定性,将资源利用控制在一定的水平之下
-
为什么使用连接池?
其实通过它的优势就能明白我们为什么要使用连接池。连接池最大的作用并不是在于对性能提升多少,而是保护服务资源。比如MySQL的最大连接数是100,但是项目在某一时刻突然涌入1000个请求,MySQL承载不了这么多的连接数,很可能会崩溃,进而影响到整个项目的访问。如果我们使用了连接池,在池中放入64个MySQL连接,那不管是多少请求过来,MySQL最多只会被占用64个连接,从而保证MySQL的安全稳定,而不会被大流量瞬间击垮。
-
连接池里的连接个数设置多少合适?
连接池里的连接个数设置一般不低于服务资源最大连接个数的三分之一,不超过三分之二。比如:MySQL设置的最大连接数(max_connections)为100,那连接池个数在50-70之间是比较合理的,设置太小没意义,太大可能会有一些不可控的问题导致连接超出。至于MySQL的最大连接数(max_connections)如何设置这个就得看机器了,服务器的CPU、内存等硬件配置都会影响服务资源的最大连接个数。
封装前的说明
本次我是在Laravel框架中借助Swoole协程来实现对Redis及MySQL连接池的封装,Swoole官方文档中也给出了使用示例,可以参考:https://wiki.swoole.com/#/coroutine/conn_pool
封装前提是已经安装好了PHP的swoole及redis的扩展
本次使用版本介绍:Laravel版本是7+,Swoole版本是4+,Redis版本是6+
封装MySQL连接池
-
在Laravel的app目录下创建文件夹Pool用于放置所有代码,然后在Pool目录下再创建文件夹Core用于放置核心类,在Core目录下创建类文件MySQL.php,内容如下
pool = $pool; } /** * 从连接池中获取连接 * @return mixed */ public function connection() { return $this->pool->get(); } /** * 向连接池中归还连接 * @param $pdo */ public function put($pdo) { $this->pool->put($pdo); } /** * MySQL查询操作 * @param $sql * @return string */ public function query($sql) { try { $pdo = $this->connection(); $return = $pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC); // 归还连接,很重要!!! $this->put($pdo); return $return; } catch (\PDOException $e) { // 出现异常,归还一个空连接以保证连接池的数量平衡。很重要!!! $this->put(null); return $e->getMessage(); } } /** * MySQL增删改操作 * @param $sql * @return bool|string */ public function execute($sql) { try { $pdo = $this->connection(); $pdo->exec($sql); $this->put($pdo); return true; } catch (\PDOException $e) { $this->put(null); return $e->getMessage(); } } }
-
在Pool目录下再创建Database文件夹,存放封装的MySQL连接池,在下边创建类 DbPool.php,内容如下
app = $app; $this->init(); } /** * 创建连接池,如果需要连接多个库,将$config换成数组形式,下边在foreach中创建 * .env文件中需要配置好下边的一些参数,DB_SIZE是连接池中连接的数量 */ protected function init() { $config = (new PDOConfig()) ->withHost(env('DB_HOST')) ->withPort(env('DB_PORT')) ->withDbname(env('DB_DATABASE')) ->withUsername(env('DB_USERNAME')) ->withPassword(env('DB_PASSWORD')); $this->pool = new PDOPool($config, env('DB_SIZE')); } public function get() { return $this->pool->get(); } public function put($pdo) { $this->pool->put($pdo); } }
-
在Pool目录下创建Db类
$name(...$arguments); $channel->push($return); }); return $channel->pop(); } }
-
在服务提供者 App/Providers/AppServiceProvider.php 的 boot 方法中注册单例
use App\Pool\Core\MySQL; use App\Pool\Database\DbPool; ... public function boot() { $this->app->singleton('mysql_pool', function () { return new MySQL(new DbPool($this->app)); }); }
-
创建一个控制器,测试MySQL连接池(使用协程模拟并发100访问查询)
use App\Pool\Db; use Swoole\Coroutine; use Swoole\Coroutine\Channel; ... public function testMySQLPool() { try { $chan = new Channel(1); for ($i = 0; $i < 100; $i++) { Coroutine::create( function () use ($chan) { $chan->push(Db::query("select * from test")); } ); } return $chan->pop(); } catch (\Exception $e) { return 111; } }
访问此方法,然后在数据库中用
show processlist;
命令查看当前数据库的连接数,对比连接数和设置的连接池中连接数量,验证连接池是否有效。MySQL内部默认会建立几个连接,这几个不用管。
封装Redis连接池
-
还是在Pool/Core目录下创建Redis的核心类 CoRedis.php
RedisCommand 文件放的就是Redis的一些操作命令,比如:set(),get() 等。由于文件内容太长就不贴出来了,放个网盘链接,自己下载看吧
链接:https://pan.baidu.com/s/1eN5OjX-QPzYhlXcOgLGsHw
提取码:udilpool = $pool; } public function connection() { return $this->pool->get(); } public function put($redis) { $this->pool->put($redis); } public function __call($name, $arguments) { try { $redisConnection = $this->connection(); $redis = new RedisCommand($redisConnection); $return = $redis->$name(...$arguments); $this->put($redisConnection); return $return; } catch (\Exception $e) { $this->put(null); return $e->getMessage(); } } }
-
在Pool目录下创建Redis文件夹(还放在上边创建的Database目录下也行),创建Redis连接池类 RedisPool.php
app = $app; $this->init(); } protected function init() { $config = (new RedisConfig) ->withHost(env('REDIS_HOST')) ->withPort(env('REDIS_PORT')) ->withAuth(env('REDIS_PASSWORD')); $this->pool = new Pool($config, env('REDIS_SIZE')); } public function get() { return $this->pool->get(); } public function put($pdo) { $this->pool->put($pdo); } }
-
在Pool目录下创建 Redis.php
$name(...$arguments); $channel->push($return); }); return $channel->pop(); } }
-
在服务提供者 App/Providers/AppServiceProvider.php 的 boot 方法中注册单例
use App\Pool\Core\CoRedis; use App\Pool\Redis\RedisPool; ... public function boot() { ... $this->app->singleton('redis_pool', function () { return new CoRedis(new RedisPool($this->app)); }); }
-
在控制器中测试
use App\Pool\Redis; ... public function test_redis() { try { Redis::set('k1', 1111); $chan = new Channel(1); for ($i = 0; $i < 100; $i++) { Coroutine::create( function () use ($chan) { $chan->push(Redis::get('k1')); } ); } return $chan->pop(); } catch (\Exception $e) { return 111; } }
测试过程与MySQL一致,在Redis客户端中使用
info clients
命令来查看当前连接数