上一篇文章介绍了利用channel来实现waitgroup和进程间通信的功能,本篇文章将继续利用channel来实现一个简单的mysql连接池,并且介绍利用新的的特性:defer来实现资源的回收
为什么要实现mysql连接池?
有以下几个原因:
-
保持长连接可以节省连接相关的开销(不过由于swoole本身常驻进程,所以只要不手工close,也还是长连接了)
-
mysql本身对连接有限制,所以每个请求(协程)都建立一个连接很容易导致mysql连接被打满
下面就来看一段代码,看swoole里实现一个连接池是怎么样的简单
<?php
use Swoole\Coroutine\MySQL;
class MysqlPool
{
private static $instance;
private $pool; //连接池容器,一个channel
private $config;
/**
* @param null $config
* @return MysqlPool
* @desc 获取连接池实例
*/
public static function getInstance($config = null)
{
if (empty(self::$instance)) {
if (empty($config)) {
throw new RuntimeException("mysql config empty");
}
self::$instance = new static($config);
}
return self::$instance;
}
/**
* MysqlPool constructor.
* @param $config
* @desc 初始化,自动创建实例,需要放在workerstart中执行
*/
public function __construct($config)
{
if (empty($this->pool)) {
$this->config = $config;
$this->pool = new chan($config['pool_size']);
for ($i = 0; $i < $config['pool_size']; $i++) {
$mysql = new MySQL();
$res = $mysql->connect($config);
if ($res == false) {
//连接失败,抛弃常
throw new RuntimeException("failed to connect mysql server.");
} else {
//mysql连接存入channel
$this->put($mysql);
}
}
}
}
/**
* @param $mysql
* @desc 放入一个mysql连接入池
*/
public function put($mysql)
{
$this->pool->push($mysql);
}
/**
* @return mixed
* @desc 获取一个连接,当超时,返回一个异常
*/
public function get()
{
$mysql = $this->pool->pop($this->config['pool_get_timeout']);
if (false === $mysql) {
throw new RuntimeException("get mysql timeout, all mysql connection is used");
}
return $mysql;
}
/**
* @return mixed
* @desc 获取当时连接池可用对象
*/
public function getLength()
{
return $this->pool->length();
}
}
那如何使用呢?继续看一段代码:
<?php
require_once "mysql_pool.php";
$config = [
'host' => '127.0.0.1', //数据库ip
'port' => 3306, //数据库端口
'user' => 'root', //数据库用户名
'password' => '123456', //数据库密码
'database' => 'test', //默认数据库名
'timeout' => 0.5, //数据库连接超时时间
'charset' => 'utf8mb4', //默认字符集
'strict_type' => true, //ture,会自动表数字转为int类型
'pool_size' => '3', //连接池大小
'pool_get_timeout' => 0.5, //当在此时间内未获得到一个连接,会立即返回。(表示所以的连接都已在使用中)
];
//创建http server
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->set([
//"daemonize" => true,
"worker_num" => 1,
"log_level" => SWOOLE_LOG_ERROR,
]);
$http->on('WorkerStart', function ($serv, $worker_id) use ($config) {
//worker启动时,每个进程都初始化连接池,在onRequest中可以直接使用
try {
MysqlPool::getInstance($config);
} catch (\Exception $e) {
//初始化异常,关闭服务
echo $e->getMessage() . PHP_EOL;
$serv->shutdown();
} catch (\Throwable $throwable) {
//初始化异常,关闭服务
echo $throwable->getMessage() . PHP_EOL;
$serv->shutdown();
}
});
$http->on('request', function ($request, $response) {
//浏览器会自动发起这个请求,这也是很多人碰到的一个问题:
//为什么我浏览器打开网站,收到了两个请求?
if ($request->server['path_info'] == '/favicon.ico') {
$response->end('');
return;
}
//获取数据库
if ($request->server['path_info'] == '/list') {
go(function () use ($request, $response) {
//从池子中获取一个实例
try {
$pool = MysqlPool::getInstance();
$mysql = $pool->get();
defer(function () use ($mysql) {
//利用defer特性,可以达到协程执行完成,归还$mysql到连接池
//好处是 可能因为业务代码很长,导致乱用或者忘记把资源归还
MysqlPool::getInstance()->put($mysql);
echo "当前可用连接数:" . MysqlPool::getInstance()->getLength() . PHP_EOL;
});
$result = $mysql->query("select * from test");
$response->end(json_encode($result));
} catch (\Exception $e) {
$response->end($e->getMessage());
}
});
return;
}
//模拟timeout, 浏览器打开4个tab,都请求 http://127.0.0.1:9501/timeout,前三个应该是等10秒出结果,第四个500ms后出超时结果
//ps: chrome浏览器,需要加一个随机数,http://127.0.0.1:9501/timeout?t=0, http://127.0.0.1:9501/timeout?t=1, 因为chrome会对完全一样的url做并发请求限制
echo "get request:".time().PHP_EOL;
if ($request->server['path_info'] == '/timeout') {
go(function () use ($request, $response) {
//从池子中获取一个实例
try {
$pool = MysqlPool::getInstance();
echo "当前可用连接数:" . $pool->getLength() . PHP_EOL;
$mysql = $pool->get();
echo "当前可用连接数:" . $pool->getLength() . PHP_EOL;
defer(function () use ($mysql) {
//协程执行完成,归还$mysql到连接池
MysqlPool::getInstance()->put($mysql);
echo "当前可用连接数:" . MysqlPool::getInstance()->getLength() . PHP_EOL;
});
$result = $mysql->query("select * from test");
\Swoole\Coroutine::sleep(10); //sleep 10秒,模拟耗时操作
$response->end(json_encode($result));
} catch (\Exception $e) {
$response->end($e->getMessage());
}
});
return;
}
});
$http->start();
几个重点
-
在workerStart初始化连接,可以做一些前置判断
-
利用defer特性,以免乱用或忘记归还资源,减轻开发心智负担
修改自己的数据配置,和执行语句,然后浏览器执行:
http://127.0.0.1:9501/list 可以看到正常的结果输出
http://127.0.0.1:9501/timeout 演示连接池取和存的过程,大家也可以实验一下(注意看注释里的说明)
为什么用channel?
不用channel,直接用一个array或者sqlQueue也是可以的,用channel有几个好处
-
channel也是可被协程调度的
-
channel->pop的时候,可以设置超时,用array 或者 sqlQueue 实现就会麻烦一些
需要注意的点
-
由于swoole是多进程架构,直连mysql的话,连接数=worker_num * pool_size
-
defer需要swoole版本 >= 4.2.9
预告:本篇只是实现一个简单的mysql连接池,下篇将完善这个连接池,能够做到异常捕获和处理,以及如何处理非常经典『MySQL server has gone away』
查看原文,可以了解swoole/mysql相关的姿势
--------------伟大的分割线----------------
PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!
饭米粒只发原创或授权发表的文章,不转载网上的文章
所发的文章,均可找到原作者进行沟通。
也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。
投稿请联系:
本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)
文章评论