一、数据库连接池基本概念

所谓的数据库连接池,一般指的就是程序和数据库保持一定数量的数据库连接不断开,并且各请求的连接可以相互复用,减少重复新建数据库连接的消耗和避免在高并发的情况下出现数据库max connections等错误。自己总结一下,如果要实现一个数据库连接池,一般有几个特点:连接复用,不同的请求连接,可以放回池中,等待下个请求发分配和调用

连接数量一般维持min-max的最大最少值之间

对于空闲连接的回收

可以抗一定程度的高并发,也就是说当一次并发请求完池中所有的连接时,获取不到连接的请求可等待其他连接的释放

总结几个特性后,一个基本连接池,大致要实现下图功能:

创建连接:连接池启动后,初始化一定的空闲连接,指定为最少的连接min。当连接池为空,不够用时,创建新的连接放到池里,但不能超过指定的最大连接max数量。

连接释放:每次使用完连接,一定要调用释放方法,把连接放回池中,给其他程序或请求使用。

连接分配:连接池中用pop和push的方式对等入队和出队分配与回收。能实现阻塞分配,也就是在池空并且已创建数量大于max,阻塞一定时间等待其他请求的连接释放,超时则返回null。

连接管理:对连接池中的连接,定时检活和释放空闲连接等

二、Fpm+数据库长连接的实现利用fpm实现:例如你要实例一个100连接数的池,开启100个空闲fpm,然后每个fpm的连接都是数据库长连接。一般pm.max_spare_servers = 8这个配置项就是维持连接池的空闲数量,然后pm.max_children = 50就是最大的连接数量。和fpm的进程数量一致。

三、基于swoole的实现swoole简单介绍(更多参阅swoole官网)

swoole是一个PHP实现异步网络通信的引擎或者扩展,其中实现了很多传统PHP-fpm没有的东西,例如异步的客户端,异步Io,常驻内存,协程等等,一个个优秀的扩展,其中异步和协程等概念能应用于高并发场景。缺点是文档和入门的门槛都比较高,需要排坑。附上swoole的运行流程和进程结构图:

运行流程图

进程/线程架构图

基于swoole现实时的注意事项

首先,为了减少大家对之后运行示例代码产生不必要的天坑,先把注意事项和场景问题放前面:

1、程序中使用了协程的通信管道channel(与go的chan差不多的),其中swoole2是不支持chan->pop($timeout)中timeout超时等待的,所以必须用swoole4版本

3、笔者使用的环境为:PHP 7.1.18和swoole4作为此次开发的环境基于swoole现实连接池的方法

首先,此次利用swoole实现连接池,运用到swoole以下技术或者概念

1、连接变量池,这里可以看做一个数组或者队列,利用swoole全局变量的常驻内存特性,只要变量没主动unset掉,数组或队列中的连接对象可以一直保持,不释放。

2、协程。协程是纯用户状态的线程,通过协作的方式而不是抢占的方式来切换。首先此次的连接池两处用到协程:一个是mysql的协程客户端,为什么要用协程客户端,因为如果是用同步客户端PDO,在一个进程处理内,就算有几百个连接池,swoole worker进程中用普通的PDO方式,随便并发多少个请求,每一个请求都只能等上一个请求执行完毕,woker才处理下一个请求,这里就算阻塞了。为了让一个worker支持阻塞切换出cpu去处理其他请求,所以要用到协程的协助切换,或者异步客户端也可以,但是异步客户端使用起来嵌套太多,很不方便。swoole协程可以无感知的用同步的代码编写方式达到异步IO的效果和性能。

第二个是底层实现了协程切换和调度的channel,以下详述什么是channel

3、Coroutine/channel通道,类似于go语言的chan,支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。高并发时,容易出连接池为空时,如果用一般的array或者splqueue()作为介质存储连接对象变量,不能产生阻塞等待其他请求释放的效果,也就是说只能直接返回null.。所以这里用了一个swoole4协程中很牛逼的channel通过管道作为存储介质,它的出队方法pop($timeout)可以指定阻塞等待指定时间后返回。注意,是swoole2是没有超时timeout的参数,不适用此场景。在go语言中,如果chan等待或者push了没有消费或者生产一对一的情况,是会发生死锁。所以swoole4的timeout应该是为了避免无限等待为空channel情况而产生。

channel切换的例子:

$chan = new Channel();

go(function()use($chan)

{ echo"我是第一个协程,等待3秒内有push就执行返回" . PHP_EOL;

$p = $chan->pop(2);#1

echo"pop返回结果" . PHP_EOL;

var_dump($p);

});

go(function()use($chan){

co::sleep(1);#2

$chan->push(1);

});

echo"main" . PHP_EOL;

#1处代码会首先执行,然后遇到pop(),因为channel还是空,会等待2s。此时协程会让出cpu,跳到第二个协程执行,然后#2出睡眠1秒,push变量1进去channel后返回#1处继续执行,成功取车通过中刚push的值1.运行结果为:

如果把#2处的睡眠时间换成大于pop()的等待时间,结果是:

根据这些特性最终实现连接池的抽象封装类为:

/**

* 连接池封装.

* User: user

* Date: 2018/9/1

* Time: 13:36

*/

use Swoole\Coroutine\Channel;

abstract class AbstractPool

{

private $min;//最少连接数

private $max;//最大连接数

private $count;//当前连接数

private $connections;//连接池组

protected $spareTime;//用于空闲连接回收判断

//数据库配置

protected $dbConfig = array(

'host' => '10.0.2.2',

'port' => 3306,

'user' => 'root',

'password' => 'root',

'database' => 'test',

'charset' => 'utf8',

'timeout' => 2,

);

private $inited = false;

protected abstract function createDb();

public function __construct()

{

$this->min = 10;

$this->max = 100;

$this->spareTime = 10 * 3600;

$this->connections = new Channel($this->max + 1);

}

protected function createObject()

{

$obj = null;

$db = $this->createDb();

if ($db) {

$obj = [

'last_used_time' => time(),

'db' => $db,

];

}

return $obj;

}

/**

* 初始换最小数量连接池

* @return $this|null

*/

public function init()

{

if ($this->inited) {

return null;

}

for ($i = 0; $i < $this->min; $i++) {

$obj = $this->createObject();

$this->count++;

$this->connections->push($obj);

}

return $this;

}

public function getConnection($timeOut = 3)

{

$obj = null;

if ($this->connections->isEmpty()) {

if ($this->count < $this->max) {//连接数没达到最大,新建连接入池

$this->count++;

$obj = $this->createObject();

} else {

$obj = $this->connections->pop($timeOut);//timeout为出队的最大的等待时间

}

} else {

$obj = $this->connections->pop($timeOut);

}

return $obj;

}

public function free($obj)

{

if ($obj) {

$this->connections->push($obj);

}

}

/**

* 处理空闲连接

*/

public function gcSpareObject()

{

//大约2分钟检测一次连接

swoole_timer_tick(120000, function () {

$list = [];

/*echo "开始检测回收空闲链接" . $this->connections->length() . PHP_EOL;*/

if ($this->connections->length() < intval($this->max * 0.5)) {

echo "请求连接数还比较多,暂不回收空闲连接\n";

}#1

while (true) {

if (!$this->connections->isEmpty()) {

$obj = $this->connections->pop(0.001);

$last_used_time = $obj['last_used_time'];

if ($this->count > $this->min && (time() - $last_used_time > $this->spareTime)) {//回收

$this->count--;

} else {

array_push($list, $obj);

}

} else {

break;

}

}

foreach ($list as $item) {

$this->connections->push($item);

}

unset($list);

});

}

}同步PDO客户端下实现

<<?php

/**

* 数据库连接池PDO方式

* User: user

* Date: 2018/9/8

* Time: 11:30

*/

require "AbstractPool.php";

class MysqlPoolPdo extends AbstractPool

{

protected $dbConfig = array(

'host' => 'mysql:host=10.0.2.2:3306;dbname=test',

'port' => 3306,

'user' => 'root',

'password' => 'root',

'database' => 'test',

'charset' => 'utf8',

'timeout' => 2,

);

public static $instance;

public static function getInstance()

{

if (is_null(self::$instance)) {

self::$instance = new MysqlPoolPdo();

}

return self::$instance;

}

protected function createDb()

{

return new PDO($this->dbConfig['host'], $this->dbConfig['user'], $this->dbConfig['password']);

}

}

$httpServer = new swoole_http_server('0.0.0.0', 9501);

$httpServer->set(

['worker_num' => 1]

);

$httpServer->on("WorkerStart", function () {

MysqlPoolPdo::getInstance()->init();

});

$httpServer->on("request", function ($request, $response) {

$db = null;

$obj = MysqlPoolPdo::getInstance()->getConnection();

if (!empty($obj)) {

$db = $obj ? $obj['db'] : null;

}

if ($db) {

$db->query("select sleep(2)");

$ret = $db->query("select * from guestbook limit 1");

MysqlPoolPdo::getInstance()->free($obj);

$response->end(json_encode($ret));

}

});

$httpServer->start();

代码调用过程详解:

1、server启动时,调用init()方法初始化最少数量(min指定)的连接对象,放进类型为channelle的connections对象中。在init中循环调用中,依赖了createObject()返回连接对象,而createObject()

中是调用了本来实现的抽象方法,初始化返回一个PDO db连接。所以此时,连接池connections中有min个对象。

2、server监听用户请求,当接收发请求时,调用连接数的getConnection()方法从connections通道中pop()一个对象。此时如果并发了10个请求,server因为配置了1个worker,所以再pop到一个对象返回时,遇到sleep()的查询,因为用的连接对象是pdo的查询,此时的woker进程只能等待,完成后才能进入下一个请求。因此,池中的其余连接其实是多余的,同步客户端的请求速度只能和woker的数量有关。

3、查询结束后,调用free()方法把连接对象放回connections池中。

ab -c 10 -n 10运行的结果,单个worker处理,select sleep(2) 查询睡眠2s,同步客户端方式总共运行时间为20s以上,而且mysql的连接始终维持在一条。结果如下:

协程客户端Coroutine\MySQL方式的调用

/**

* 数据库连接池协程方式

* User: user

* Date: 2018/9/8

* Time: 11:30

*/

require "AbstractPool.php";

class MysqlPoolCoroutine extends AbstractPool

{

protected $dbConfig = array(

'host' => '10.0.2.2',

'port' => 3306,

'user' => 'root',

'password' => 'root',

'database' => 'test',

'charset' => 'utf8',

'timeout' => 10,

);

public static $instance;

public static function getInstance()

{

if (is_null(self::$instance)) {

self::$instance = new MysqlPoolCoroutine();

}

return self::$instance;

}

protected function createDb()

{

$db = new Swoole\Coroutine\Mysql();

$db->connect(

$this->dbConfig

);

return $db;

}

}

$httpServer = new swoole_http_server('0.0.0.0', 9501);

$httpServer->set(

['worker_num' => 1]

);

$httpServer->on("WorkerStart", function () {

//MysqlPoolCoroutine::getInstance()->init()->gcSpareObject();

MysqlPoolCoroutine::getInstance()->init();

});

$httpServer->on("request", function ($request, $response) {

$db = null;

$obj = MysqlPoolCoroutine::getInstance()->getConnection();

if (!empty($obj)) {

$db = $obj ? $obj['db'] : null;

}

if ($db) {

$db->query("select sleep(2)");

$ret = $db->query("select * from guestbook limit 1");

MysqlPoolCoroutine::getInstance()->free($obj);

$response->end(json_encode($ret));

}

});

$httpServer->start();

代码调用过程详解

1、同样的,协程客户端方式下的调用,也是实现了之前封装好的连接池类AbstractPool.php。只是createDb()的抽象方法用了swoole内置的协程客户端去实现。

2、server启动后,初始化都和同步一样。不一样的在获取连接对象的时候,此时如果并发了10个请求,同样是配置了1个worker进程在处理,但是在第一请求到达,pop出池中的一个连接对象,执行到query()方法,遇上sleep阻塞时,此时,woker进程不是在等待select的完成,而是切换到另外的协程去处理下一个请求。完成后同样释放对象到池中。当中有重点解释的代码段中getConnection()中。

public function getConnection($timeOut = 3)

{

$obj = null;

if ($this->connections->isEmpty()) {

if ($this->count < $this->max) {//连接数没达到最大,新建连接入池

$this->count++;

$obj = $this->createObject();#1

} else {

$obj = $this->connections->pop($timeOut);#2

}

} else {

$obj = $this->connections->pop($timeOut);#3

}

return $obj;

}

当调用到getConnection()时,如果此时由于大量并发请求过多,连接池connections为空,而没达到最大连接max数量时时,代码运行到#1处,调用了createObject(),新建连接返回;但如果连接池connections为空,而到达了最大连接数max时,代码运行到了#2处,也就是$this->connections->pop($timeOut),此时会阻塞$timeOut的时间,如果期间有链接释放了,会成功获取到,然后协程返回。超时没获取到,则返回false。

3、最后说一下协程Mysql客户端一项重要配置,那就是代码里$dbConfig中timeout值的配置。这个配置是意思是最长的查询等待时间。可以看一个例子说明下:

go(function () {

$start = microtime(true);

$db = new Swoole\Coroutine\MySQL();

$db->connect([

'host' => '10.0.2.2',

'port' => 3306,

'user' => 'root',

'password' => 'root',

'database' => 'test',

'timeout' => 4#1

]);

$db->query("select sleep(5)");

echo "我是第一个sleep五秒之后\n";

$ret = $db->query("select user from guestbook limit 1");#2

var_dump($ret);

$use = microtime(true) - $start;

echo "协程mysql输出用时:" . $use . PHP_EOL;

});

#1处代码,如果timeout配了4s查询超时,而第一条查询select sleep(5)阻塞后,协程切换到下一条sql的执行,其实$db并不能执行成功,因为用一个连接,同一个协程中,其实执行是同步的,所以此时第二条查询在等待4s超时后,没获取到db的连接执行,就会执行失败。而如果第一条查询执行的时间少于这个timeout,那么会执行查询成功。猜猜上面执行用时多少?结果如下:

如果把timeout换成6s呢,结果如下:

所以要注意的是,协程的客户端内执行其实是同步的,不要理解为异步,它只是遇到IO阻塞时能让出执行权,切换到其他协程而已,不能和异步混淆。

ab -c 10 -n 10运行的结果,单个worker处理,select sleep(2) 查询睡眠2s,协程客户端方式总共运行时间为2s多。结果如下:

数据库此时的连接数为10条(show full PROCESSLIST):

再尝试 ab -c 200 -n 1000 http://127.0.0.1:9501/,200多个并发的处理,时间是20多秒,mysql连接数达到指定的最大值100个。结果如下:

四、后言

现在连接池基本实现了高并发时的连接分配和控制,但是还有一些细节要处理,例如:并发时,建立了max个池对象,不能一直在池中维护这么多,要在请求空闲时,把连接池的数量维持在一个空闲值内。这里是简单做了gcSpareObject()的方法实现空闲处理。直接在初始化woker的时候调用:MysqlPoolCoroutine::getInstance()->init()->gcSpareObject();就会定时检测回收。问题是如何判断程序比较空闲,值得再去优化。

定时检测连接时候是活的,剔除死链

假如程序忘记调用free()释放对象到池,是否有更好方法避免这种情况?

本书做了一个很好的归纳。同时也欢迎点击:正在跳转加入,任何问题可以一起讨论,也欢迎大家一起踊跃发言!

swoole mysql 并发_Swoole4 如何打造高并发的PHP7协程Mysql连接池?相关推荐

  1. android 高并发服务端,GitHub - android-coco/chat: 支持10万人同时在线 Go语言打造高并发web即时聊天(IM)应用...

    IM 支持10万人同时在线 Go语言打造高并发web即时聊天(IM)应用 部署前准备 配置文件 config/config.yml 样例: # 服务端监听配置 service: port: :8181 ...

  2. 如何打造高并发互动小程序,护航腾讯游戏年度发布会?

    导语 | 2020腾讯游戏年度发布会以线上直播的形式进行,区别于往年的线下toB模式,线上直播面对的是所有玩家,所以在直播内容.直播落地等方面,都要考虑如何做得更加toC.参与量如此大.互动如此多.内 ...

  3. 如何处理几十万条并发数据_Swoole 如何处理高并发以及异步 I/O 的实现

    Swoole 介绍 1.swoole提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis, 数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件 ...

  4. 《Java并发编程入门与高并发面试》or 《Java并发编程与高并发解决方案》笔记

    <Java并发编程入门与高并发面试>or <Java并发编程与高并发解决方案>笔记 参考文章: (1)<Java并发编程入门与高并发面试>or <Java并发 ...

  5. swoole mysql 协程_Swoole 协程 MySQL 客户端与异步回调 MySQL 客户端的对比

    Swoole 协程 MySql 客户端与 异步回调 MySql 客户端的对比 为什么要对比这两种不同模式的客户端? 异步 MySQL 回调客户端是虽然在 Swoole 1.8.6 版本就已经发布了, ...

  6. mysql文章浏览计数_高并发文章浏览量计数系统设计

    最近因为个人网站的文章浏览量计数在Chrome浏览器下有BUG,所以打算重新实现这个功能. 原本的实现很简单,每次点击文章详情页的时候,前端会发送一个GET请求articles/id获取一篇文章详情. ...

  7. MySQL在大数据、高并发场景下的SQL语句优化

    转载自:https://segmentfault.com/a/1190000007311574 本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓"大数据&quo ...

  8. JAVA秒杀mysql层实现_Java商城高并发秒杀系统架构分析设计与开发实战

    课程大纲 1-1课程整体介绍.mp4 1-2核心技术列表.mp4 1-3课程要求与收益.mp4 1-4系统的整体演示.mp4 2-1微服务项目的搭建-SpringBoot搭建多模块项目一.mp4 2- ...

  9. mysql高并发不用事务_Mysql高并发加锁事务处理

    # Mysql高并发加锁事务处理 MySQL 使用 SELECT - FOR UPDATE 做事务写入前的确认 以MySQL 的InnoDB 为例,预设的 Tansaction isolation l ...

最新文章

  1. C#中HtmlAgilityPack判断是否包含或不包含指定的属性或值
  2. php 定义title,HTML5中对title属性的定义与规定
  3. 当 Vue 处理数组与处理纯对象的方式一样
  4. [BZOJ1087][SCOI2005]互不侵犯King
  5. Git之深入解析如何使用Git的分布式工作流程与如何管理多人开发贡献的项目
  6. Daily Scrum 11.18
  7. php if !=,php If else 用法
  8. FinTech专题:支付平台建设资金底线防火墙的杀手级设计方案
  9. 用Raspberry Pi消除害虫
  10. 一些意想不到的bug
  11. TX2开发板Ubuntu16.04设置静态IP
  12. 专用于个人简历的latex模板
  13. 校验码——CRC循环冗余校验码,码距,例题
  14. ML(5)——神经网络1(神经元模型与激活函数)
  15. Ansible—— 29. 通过set_fact模块定义变量
  16. (Tekla Structures二次开发)使用VS创建Tekla Structures插件的模板
  17. 肖申克的救赎-救赎自己的心灵
  18. 郑州大学现代远程教育《商务礼仪》课程考核作业要求院校通智慧校园
  19. 鉴权kafka生产端(SCRAM)
  20. 基于单片机红外对管测风速控制系统设计(毕设课设)

热门文章

  1. androidstuido 查看logs_使用 Logcat 写入和查看日志
  2. python移动文件中某个内容_如果python中的某些文件类型,则移动文件并创建目录...
  3. 服务器设置为自动登录,Windows Server 2008 R2怎样设置自动登陆(登录)
  4. java 反射 new区别_JAVA的newInstance()和new的区别(JAVA反射机制,通过类名来获取该类的实例化对象)...
  5. word如何一键全选_这些超实用的word技巧,个个让你事半功倍
  6. spring.profiles.active配置了没生效_一文带你彻底学会 Git Hooks 配置
  7. .bin 文件用excel文件打开_用PYTHON读写excel文件
  8. fprintf函数的用法_MATLAB的21种特殊符号,总有一种你不知道的用法
  9. matlab中inband函数,pjsip 实现 DTMF 数据获取,并解析按键信息
  10. 基本数据结构篇(三万字总结)