連接池
-
什么是連接池鞭莽?
連接池是創(chuàng)建和管理一個連接的緩沖池的技術(shù)普气,緩沖池中的連接可以被任何需要他們的線程使用。
一個服務(wù)端資源的連接數(shù)量是有限的柑船,比如:Redis的maxclients帽撑、MySQL的max_connections、PHP-FPM的max_children鞍时,start_servers 等配置參數(shù)亏拉,都是用來設(shè)置連接數(shù)的。
連接池的原理就是在資源初始化時將一定數(shù)量的連接存放在連接池中逆巍,誰用誰取及塘,用完后再放回去,實現(xiàn)連接的復(fù)用蒸苇。如果超出連接池容量磷蛹,會進行排隊等待或者直接丟棄。
-
連接池的優(yōu)勢溪烤?
- 減少連接創(chuàng)建時間:連接池中的連接是已經(jīng)準(zhǔn)備好的味咳、可重復(fù)使用的
- 簡化的編程模式:當(dāng)使用連接池時,每一個單獨的線程能夠像創(chuàng)建一個自己的PDO連接一樣操作
- 控制資源的使用:增強系統(tǒng)的穩(wěn)定性檬嘀,將資源利用控制在一定的水平之下
-
為什么使用連接池槽驶?
其實通過它的優(yōu)勢就能明白我們?yōu)槭裁匆褂眠B接池。連接池最大的作用并不是在于對性能提升多少鸳兽,而是保護服務(wù)資源掂铐。比如MySQL的最大連接數(shù)是100,但是項目在某一時刻突然涌入1000個請求揍异,MySQL承載不了這么多的連接數(shù)全陨,很可能會崩潰,進而影響到整個項目的訪問衷掷。如果我們使用了連接池辱姨,在池中放入64個MySQL連接,那不管是多少請求過來戚嗅,MySQL最多只會被占用64個連接雨涛,從而保證MySQL的安全穩(wěn)定,而不會被大流量瞬間擊垮懦胞。
-
連接池里的連接個數(shù)設(shè)置多少合適替久?
連接池里的連接個數(shù)設(shè)置一般不低于服務(wù)資源最大連接個數(shù)的三分之一,不超過三分之二躏尉。比如:MySQL設(shè)置的最大連接數(shù)(max_connections)為100蚯根,那連接池個數(shù)在50-70之間是比較合理的,設(shè)置太小沒意義胀糜,太大可能會有一些不可控的問題導(dǎo)致連接超出颅拦。至于MySQL的最大連接數(shù)(max_connections)如何設(shè)置這個就得看機器了吼具,服務(wù)器的CPU、內(nèi)存等硬件配置都會影響服務(wù)資源的最大連接個數(shù)矩距。
封裝前的說明
本次我是在Laravel框架中借助Swoole協(xié)程來實現(xiàn)對Redis及MySQL連接池的封裝,Swoole官方文檔中也給出了使用示例怖竭,可以參考:https://wiki.swoole.com/#/coroutine/conn_pool
封裝前提是已經(jīng)安裝好了PHP的swoole及redis的擴展
本次使用版本介紹:Laravel版本是7+锥债,Swoole版本是4+,Redis版本是6+
封裝MySQL連接池
-
在Laravel的app目錄下創(chuàng)建文件夾Pool用于放置所有代碼痊臭,然后在Pool目錄下再創(chuàng)建文件夾Core用于放置核心類哮肚,在Core目錄下創(chuàng)建類文件MySQL.php,內(nèi)容如下
<?php namespace App\Pool\Core; class MySQL { /** * 在實例化時獲取到的連接池對象 * @var */ protected $pool; public function __construct($pool) { $this->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); // 歸還連接广匙,很重要T侍恕!鸦致! $this->put($pdo); return $return; } catch (\PDOException $e) { // 出現(xiàn)異常潮剪,歸還一個空連接以保證連接池的數(shù)量平衡。很重要7滞佟?古觥! $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目錄下再創(chuàng)建Database文件夾绽乔,存放封裝的MySQL連接池弧蝇,在下邊創(chuàng)建類 DbPool.php,內(nèi)容如下
<?php namespace App\Pool\Database; use Illuminate\Foundation\Application; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; class DbPool { protected $app; protected $pool; public function __construct(Application $app) { $this->app = $app; $this->init(); } /** * 創(chuàng)建連接池折砸,如果需要連接多個庫看疗,將$config換成數(shù)組形式,下邊在foreach中創(chuàng)建 * .env文件中需要配置好下邊的一些參數(shù)睦授,DB_SIZE是連接池中連接的數(shù)量 */ 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目錄下創(chuàng)建Db類
<?php namespace App\Pool; use Swoole\Coroutine; use Swoole\Coroutine\Channel; use Swoole\Runtime; class Db { /** * 獲取驅(qū)動 */ public static function getDriver() { return app('mysql_pool'); } /** * @param $name * @param $arguments * @return mixed */ public static function __callStatic($name, $arguments) { // 開啟一鍵協(xié)程化 Runtime::enableCoroutine(); // 實例化通道两芳,用于協(xié)程間通訊 $channel = new Channel(1); Coroutine::create(function () use ($channel, $name, $arguments) { $pdo = self::getDriver(); $return = $pdo->$name(...$arguments); $channel->push($return); }); return $channel->pop(); } }
-
在服務(wù)提供者 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)); }); }
-
創(chuàng)建一個控制器,測試MySQL連接池(使用協(xié)程模擬并發(fā)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; } }
訪問此方法睹逃,然后在數(shù)據(jù)庫中用
show processlist;
命令查看當(dāng)前數(shù)據(jù)庫的連接數(shù)盗扇,對比連接數(shù)和設(shè)置的連接池中連接數(shù)量,驗證連接池是否有效沉填。MySQL內(nèi)部默認(rèn)會建立幾個連接疗隶,這幾個不用管。
封裝Redis連接池
-
還是在Pool/Core目錄下創(chuàng)建Redis的核心類 CoRedis.php
RedisCommand 文件放的就是Redis的一些操作命令翼闹,比如:set()斑鼻,get() 等。由于文件內(nèi)容太長就不貼出來了猎荠,放個網(wǎng)盤鏈接坚弱,自己下載看吧
鏈接:https://pan.baidu.com/s/1eN5OjX-QPzYhlXcOgLGsHw
提取碼:udil<?php namespace App\Pool\Core; class CoRedis { protected $pool; public function __construct($pool) { $this->pool = $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目錄下創(chuàng)建Redis文件夾(還放在上邊創(chuàng)建的Database目錄下也行)蜀备,創(chuàng)建Redis連接池類 RedisPool.php
<?php namespace App\Pool\Redis; use Illuminate\Foundation\Application; use Swoole\Database\RedisConfig; use Swoole\Database\RedisPool as Pool; class RedisPool { protected $app; protected $pool; public function __construct(Application $app) { $this->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目錄下創(chuàng)建 Redis.php
<?php namespace App\Pool; use Swoole\Coroutine; use Swoole\Coroutine\Channel; use Swoole\Runtime; class Redis { public static function getDriver() { return app('redis_pool'); } public static function __callStatic($name, $arguments) { Runtime::enableCoroutine(); $channel = new Channel(1); Coroutine::create(function () use ($channel, $name, $arguments) { $redis = self::getDriver(); $return = $redis->$name(...$arguments); $channel->push($return); }); return $channel->pop(); } }
-
在服務(wù)提供者 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
命令來查看當(dāng)前連接數(shù)