Swoole封装MySQL与Redis的连接池

连接池

  1. 什么是连接池?

    连接池是创建和管理一个连接的缓冲池的技术,缓冲池中的连接可以被任何需要他们的线程使用。

    一个服务端资源的连接数量是有限的,比如:Redis的maxclients、MySQL的max_connections、PHP-FPM的max_children,start_servers 等配置参数,都是用来设置连接数的。

    连接池的原理就是在资源初始化时将一定数量的连接存放在连接池中,谁用谁取,用完后再放回去,实现连接的复用。如果超出连接池容量,会进行排队等待或者直接丢弃。

  2. 连接池的优势?

    • 减少连接创建时间:连接池中的连接是已经准备好的、可重复使用的
    • 简化的编程模式:当使用连接池时,每一个单独的线程能够像创建一个自己的PDO连接一样操作
    • 控制资源的使用:增强系统的稳定性,将资源利用控制在一定的水平之下
  3. 为什么使用连接池?

    其实通过它的优势就能明白我们为什么要使用连接池。连接池最大的作用并不是在于对性能提升多少,而是保护服务资源。比如MySQL的最大连接数是100,但是项目在某一时刻突然涌入1000个请求,MySQL承载不了这么多的连接数,很可能会崩溃,进而影响到整个项目的访问。如果我们使用了连接池,在池中放入64个MySQL连接,那不管是多少请求过来,MySQL最多只会被占用64个连接,从而保证MySQL的安全稳定,而不会被大流量瞬间击垮。

  4. 连接池里的连接个数设置多少合适?

    连接池里的连接个数设置一般不低于服务资源最大连接个数的三分之一,不超过三分之二。比如: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连接池

  1. 在Laravel的app目录下创建文件夹Pool用于放置所有代码,然后在Pool目录下再创建文件夹Core用于放置核心类,在Core目录下创建类文件MySQL.php,内容如下

    <?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);
                // 归还连接,很重要!!!
                $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();
            }
        }
    }
    
  2. 在Pool目录下再创建Database文件夹,存放封装的MySQL连接池,在下边创建类 DbPool.php,内容如下

    <?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();
        }
    
        /**
         * 创建连接池,如果需要连接多个库,将$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);
        }
    }
    
  3. 在Pool目录下创建Db类

    <?php
    
    namespace App\Pool;
    
    
    use Swoole\Coroutine;
    use Swoole\Coroutine\Channel;
    use Swoole\Runtime;
    
    class Db
    {
        /**
         * 获取驱动
         */
        public static function getDriver()
        {
            return app('mysql_pool');
        }
    
        /**
         * @param $name
         * @param $arguments
         * @return mixed
         */
        public static function __callStatic($name, $arguments)
        {
            // 开启一键协程化
            Runtime::enableCoroutine();
            // 实例化通道,用于协程间通讯
            $channel = new Channel(1);
            Coroutine::create(function () use ($channel, $name, $arguments) {
                $pdo = self::getDriver();
                $return = $pdo->$name(...$arguments);
                $channel->push($return);
            });
    
            return $channel->pop();
        }
    }
    
  4. 在服务提供者 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));
        });
    }
    
  5. 创建一个控制器,测试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连接池

  1. 还是在Pool/Core目录下创建Redis的核心类 CoRedis.php

    RedisCommand 文件放的就是Redis的一些操作命令,比如:set(),get() 等。由于文件内容太长就不贴出来了,放个网盘链接,自己下载看吧

    链接: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();
            }
        }
    }
    
  2. 在Pool目录下创建Redis文件夹(还放在上边创建的Database目录下也行),创建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);
        }
    }
    
  3. 在Pool目录下创建 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();
        }
    }
    
  4. 在服务提供者 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));
         });
    }
    
  5. 在控制器中测试

    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 命令来查看当前连接数

最终的目录结构展示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容