0

0

PHP API中,MYSQL与MYSQLI的持久连接区别

php中文网

php中文网

发布时间:2016-06-21 08:48:50

|

1110人浏览过

|

来源于php中文网

原创

很久很久以前,我也是因为工作上的bug,研究了php mysql client的连接驱动mysqlnd 与libmysql之间的区别php与mysql通讯那点事,这次又遇到一件跟他们有联系的事情,mysqli与mysql持久链接的区别。写出这篇文章,用了好一个多月,其一是我太懒了,其二是工作也比较忙。最近才能腾出时间,来做这些事情。每次做总结,都要认真阅读源码,理解含义,测试验证,来确认这些细节。而每一个步骤都需要花费很长的时间,而且,还不能被打断。一旦被打断了,都需要很长时间去温习上下文。也故意强迫自己写这篇总结,改改自己的惰性。

在我和我的小伙伴们如火如荼的开发、测试时发生了“mysql server too many connections”的错误,稍微排查了一下,发现是php后台进程建立了大量的链接,而没有关闭。服务器环境大约如下php5.3.x 、mysqli api、mysqlnd 驱动。代码情况是这样:


//后台进程A
/*
配置信息
'mysql'=>array(
     'driver'=>'mysqli',
//   'driver'=>'pdo',
//   'driver'=>'mysql',
     'host'=>'192.168.111.111',
     'user'=>'root',
     'port'=>3306,
     'dbname'=>'dbname',
     'socket'=>'',
     'pass'=>'pass',
     'persist'=>true,    //下面有提到哦,这是持久链接的配置
    ),
*/
$config=Yaf_Registry::get('config');
$driver = Afx_Db_Factory::DbDriver($config['mysql']['driver']);     //mysql  mysqli
$driver::debug($config['debug']);     //注意这里
$driver->setConfig($config['mysql']);     //注意这里
Afx_Module::Instance()->setAdapter($driver);     //注意这里,哪里不舒服,就注意看哪里。

$queue=Afx_Queue::Instance();
$combat = new CombatEngine();
$Role = new Role(1,true);
$idle_max=isset($config['idle_max'])?$config['idle_max']:1000;
while(true)
{
    $data = $queue->pop(MTypes::ECTYPE_COMBAT_QUEUE, 1);
    if(!$data){
        usleep(50000);    //休眠0.05秒
         ++$idle_count;
        if($idle_count>=$idle_max)
        {
            $idle_count=0;
             Afx_Db_Factory::ping();
        }
        continue;
    }
    $idle_count=0;
    $Role->setId($data['attacker']['role_id']);
    $Property   = $Role->getModule('Property');
    $Mounts     = $Role->getModule('Mounts');
    //............
    unset($Property, $Mounts/*.....*/);
}

 

从这个后台进程代码中,可以看出“$Property”变量以及“$Mounts”变量频繁被创建,销毁。而ROLE对象的getModule方法是这样写的


//ROLE对象的getModule方法
class Role extends Afx_Module_Abstract
{
    public function getModule ($member_class)
    {
        $property_name = '__m' . ucfirst($member_class);
        if (! isset($this->$property_name))
        {
            $this->$property_name = new $member_class($this);
        }
        return $this->$property_name;
    }
}
//Property 类
class Property extends Afx_Module_Abstract
{
    public function __construct ($mRole)
    {
        $this->__mRole = $mRole;
    }
}

 

可以看出getModule方法只是模拟单例,new了一个新对象返回,而他们都继承了Afx_Module_Abstract类。Afx_Module_Abstract类大约代码如下:


abstract class Afx_Module_Abstract
{
    public function setAdapter ($_adapter)
    {
        $this->_adapter = $_adapter;
    }
}

 

类Afx_Module_Abstract中关键代码如上,跟DB相关的,就setAdapter一个方法,回到“后台进程A”,setAdapter方法是将Afx_Db_Factory::DbDriver($config['mysql']['driver'])的返回,作为参数传了进来。继续看下Afx_Db_Factory类的代码


class Afx_Db_Factory
{
    const DB_MYSQL = 'mysql';
    const DB_MYSQLI = 'mysqli';
    const DB_PDO = 'pdo';

    public static function DbDriver ($type = self::DB_MYSQLI)
    {
        switch ($type)
        {
            case self::DB_MYSQL:
                $driver = Afx_Db_Mysql_Adapter::Instance();
                break;
            case self::DB_MYSQLI:
                $driver = Afx_Db_Mysqli_Adapter::Instance();    //走到这里了
                break;
            case self::DB_PDO:
                $driver = Afx_Db_Pdo_Adapter::Instance();
                break;
            default:
                break;
        }
        return $driver;
    }
}

 

一看就知道是个工厂类,继续看真正的DB Adapter部分代码

立即学习PHP免费学习笔记(深入)”;

 


class Afx_Db_Mysqli_Adapter implements Afx_Db_Adapter
{
    public static function Instance ()
    {
        if (! self::$__instance instanceof Afx_Db_Mysqli_Adapter)
        {
            self::$__instance = new self();    //这里是单例模式,为何新生成了一个mysql的链接呢?
        }
        return self::$__instance;
    }

    public function setConfig ($config)
    {
        $this->__host = $config['host'];
        //...
        $this->__user = $config['user'];
        $this->__persist = $config['persist'];
        if ($this->__persist == TRUE)
        {
            $this->__host = 'p:' . $this->__host;    //这里为持久链接做了处理,支持持久链接
        }
        $this->__config = $config;
    }

    private function __init ()
    {

        $this->__link = mysqli_init();
        $this->__link->set_opt(MYSQLI_OPT_CONNECT_TIMEOUT, $this->__timeout);
        $this->__link->real_connect($this->__host, $this->__user, $this->__pass, $this->__dbname, $this->__port, $this->__socket);
        if ($this->__link->errno == 0)
        {
            $this->__link->set_charset($this->__charset);
        } else
        {
            throw new Afx_Db_Exception($this->__link->error, $this->__link->errno);
        }
    }
}

 

从上面的代码可以看到,我们已经启用长链接了啊,为何频繁建立了这么多链接呢?为了模拟重现这个问题,我在本地开发环境进行测试,无论如何也重现不了,对比了下环境,我的开发环境是windows7、php5.3.x、mysql、libmysql,跟服务器上的不一致,问题很可能出现在mysql跟mysqli的API上,或者是libmysql跟mysqlnd的问题上。为此,我又小心翼翼的翻开PHP源码(5.3.x最新的),终于功夫不负有心人,找到了这些问题的原因。

 


//在文件ext\mysql\php_mysql.c的907-916行
//mysql_connect、mysql_pconnect都调用它,区别是持久链接标识就是persistent为false还是true
static void php_mysql_do_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent)
{
/* hash it up */
Z_TYPE(new_le) = le_plink;
new_le.ptr = mysql;
//注意下面的if里面的代码
if (zend_hash_update(&EG(persistent_list), hashed_details, hashed_details_length+1, (void *) &new_le, sizeof(zend_rsrc_list_entry), NULL)==FAILURE) {
    free(mysql);
    efree(hashed_details);
    MYSQL_DO_CONNECT_RETURN_FALSE();
}
MySG(num_persistent)++;
MySG(num_links)++;
}

 

从mysql_pconnect的代码中,可以看到,当php拓展mysql api与mysql server建立TCP链接后,就立刻将这个链接存入persistent_list中,下次建立链接是,会先从persistent_list里查找是否存在同IP、PORT、USER、PASS、CLIENT_FLAGS的链接,存在则用它,不存在则新建。

而php的mysqli拓展中,不光用了一个persistent_list来存储链接,还用了一个free_link来存储当前空闲的TCP链接。当查找时,还会判断是否在空闲的free_link链表中存在,存在了才使用这个TCP链接。而在mysqli_closez之后或者RSHUTDOWN后,才将这个链接push到free_links中。(mysqli会查找同IP,PORT、USER、PASS、DBNAME、SOCKET来作为同一标识,跟mysql不同的是,没了CLIENT,多了DBNAME跟SOCKET,而且IP还包括长连接标识“p”)

ChatGPT Website Builder
ChatGPT Website Builder

ChatGPT网站生成器,AI对话快速生成网站

下载

//文件ext\mysqli\mysqli_nonapi.c 172行左右   mysqli_common_connect创建TCP链接(mysqli_connect函数调用时)
do {
    if (zend_ptr_stack_num_elements(&plist->free_links)) {
        mysql->mysql = zend_ptr_stack_pop(&plist->free_links);    //直接pop出来,同一个脚本的下一个mysqli_connect再次调用时,就找不到它了

        MyG(num_inactive_persistent)--;
        /* reset variables */

        #ifndef MYSQLI_NO_CHANGE_USER_ON_PCONNECT
            if (!mysqli_change_user_silent(mysql->mysql, username, passwd, dbname, passwd_len)) {    //(让你看时,你再看)注意看这里mysqli_change_user_silent
        #else
            if (!mysql_ping(mysql->mysql)) {
        #endif
        #ifdef MYSQLI_USE_MYSQLND
            mysqlnd_restart_psession(mysql->mysql);
        #endif
}
//文件ext\mysqli\mysqli_api.c 585-615行
/* {{{ php_mysqli_close */
void php_mysqli_close(MY_MYSQL * mysql, int close_type, int resource_status TSRMLS_DC)
{
	if (resource_status > MYSQLI_STATUS_INITIALIZED) {
		MyG(num_links)--;
	}

	if (!mysql->persistent) {
		mysqli_close(mysql->mysql, close_type);
	} else {
		zend_rsrc_list_entry *le;
		if (zend_hash_find(&EG(persistent_list), mysql->hash_key, strlen(mysql->hash_key) + 1, (void **)&le) == SUCCESS) {
			if (Z_TYPE_P(le) == php_le_pmysqli()) {
				mysqli_plist_entry *plist = (mysqli_plist_entry *) le->ptr;
#if defined(MYSQLI_USE_MYSQLND)
				mysqlnd_end_psession(mysql->mysql);
#endif
				zend_ptr_stack_push(&plist->free_links, mysql->mysql);    //这里在push回去,下次又可以用了

				MyG(num_active_persistent)--;
				MyG(num_inactive_persistent)++;
			}
		}
		mysql->persistent = FALSE;
	}
	mysql->mysql = NULL;

	php_clear_mysql(mysql);
}
/* }}} */

 

MYSQLI为什么要这么做?为什么同一个长连接不能在同一个脚本中复用?
在C函数mysqli_common_connect中看到了有个mysqli_change_user_silent的调用,如上代码,mysqli_change_user_silent对应这libmysql的mysql_change_user或mysqlnd的mysqlnd_change_user_ex,他们都是调用了C API的mysql_change_user来清理当前TCP链接的一些临时的会话变量,未完整写的提交回滚指令,锁表指令,临时表解锁等等(这些指令,都是mysql server自己决定完成,不是php 的mysqli 判断已发送的sql指令然后做响应决定),见手册的说明The mysqli Extension and Persistent Connections。这种设计,是为了这个新特性,而mysql拓展,不支持这个功能。

从这些代码的浅薄里理解上来看,可以理解mysqli跟mysql的持久链接的区别了,这个问题,可能大家理解起来比较吃力,我后来搜了下,也发现了一个因为这个原因带来的疑惑,大家看这个案例,可能理解起来就非常容易了。Mysqli persistent connect doesn’t work回答者没具体到mysqli底层实现,实际上也是这个原因。 代码如下:



 

查看进程列表里是这样的结果:


netstat -an  grep 192.168.1.40:3306
tcp        0      0 192.168.1.6:52441       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52454       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52445       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52443       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52446       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52449       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52452       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52442       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52450       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52448       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52440       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52447       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52444       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52451       192.168.1.40:3306       ESTABLISHED
tcp        0      0 192.168.1.6:52453       192.168.1.40:3306       ESTABLISHED

 

这样看代码,就清晰多了,验证我的理解对不对也比较简单,这么一改就看出来了


for ($i = 0; $i < 15; $i++) {
    $links[$i] =  mysqli_connect('p:192.168.1.40', 'USER', 'PWD', 'DB', 3306);
    var_dump(mysqli_thread_id($links[$i]));    //如果你担心被close掉了,这是新建的TCP链接,那么你可以打印下thread id,看看是不是同一个ID,就区分开了
    mysqli_close($links[$i])
}
/*
结果如下:
root@cnxct:/home/cfc4n# netstat -antp grep 3306grep -v "php-fpm"
tcp        0      0 192.168.61.150:55148    192.168.71.88:3306      ESTABLISHED 5100/php5   
root@cnxct:/var/www# /usr/bin/php5 4.php 
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
int(224218)
*/

 

如果你担心被close掉了,这是新建的TCP链接,那么你可以打印下thread id,看看是不是同一个ID,就清楚了。(虽然我没回复这个帖子,但不能证明我很坏。)以上是CLI模式时的情况。在FPM模式下时,每个页面请求都会由单个fpm子进程处理。这个子进程将负责维护php与mysql server建立的长链接,故当你多次访问此页面,来确认是不是同一个thread id时,可能会分别分发给其他fpm子进程处理,导致看到的结果不一样。但最终,每个fpm子进程都会分别维持这些TCP链接。

总体来说,mysqli拓展跟mysql拓展的区别是下面几条

  • 持久链接建立方式,mysqli是在host前面增加“p:”两个字符;mysql使用mysql_pconnect函数;。
  • mysqli建立的持久链接,必须在mysqli_close之后,才会下面的代码复用,或者RSHOTDOWN之后,被下一个请求复用;mysql的长连接,可以立刻被复用
  • mysqli建立持久链接时,会自动清理上一个会话变量、回滚事务、表解锁、释放锁等操作;mysql不会。
  • mysqli判断是否为同一持久链接标识是IP,PORT、USER、PASS、DBNAME、SOCKET;mysql是IP、PORT、USER、PASS、CLIENT_FLAGS

好了,知道这个原因,那我们文章开头提到的问题就好解决了,大家肯定第一个想到的是在类似Property的类中,__destruct析构函数中增加一个mysqli_close方法,当被销毁时,就调用关闭函数,把持久链接push到free_links里。如果你这么想,我只能恭喜你,答错了,最好的解决方案就是压根不让它创建这么多次。同事dietoad同学给了个解决方案,对DB ADAPTER最真正单例,并且,可选是否新创建链接。如下代码:


// DB FACTORY
class Afx_Db_Factory
{
    const DB_MYSQL = 'mysql';
    const DB_MYSQLI = 'mysqli';
    const DB_PDO = 'pdo';

    static $drivers = array(
        'mysql'=>array(),'mysqli'=>array(),'pdo'=>array()
    );


    public static function DbDriver ($type = self::DB_MYSQLI, $create = FALSE)    //新增$create 参数
    {
        $driver = NULL;
        switch ($type)
        {
            case self::DB_MYSQL:
                $driver = Afx_Db_Mysql_Adapter::Instance($create);
                break;
            case self::DB_MYSQLI:
                $driver = Afx_Db_Mysqli_Adapter::Instance($create);
                break;
            case self::DB_PDO:
                $driver = Afx_Db_Pdo_Adapter::Instance($create);
                break;
            default:
                break;
        }
        self::$drivers[$type][] = $driver;
        return $driver;
    }
}

//mysqli adapter
class Afx_Db_Mysqli_Adapter implements Afx_Db_Adapter
{
    public static function Instance ($create = FALSE)
    {
        if ($create)
        {
            return new self();  //新增$create参数的判断
        }
        if (! self::$__instance instanceof Afx_Db_Mysqli_Adapter)
        {
            self::$__instance = new self();
        }
        return self::$__instance;
    }
}

 

看来,开发环境跟运行环境一致是多么的重要,否则就不会遇到这些问题了。不过,如果没遇到这么有意思的问题,岂不是太可惜了




PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
2026春节习俗大全
2026春节习俗大全

本专题整合了2026春节习俗大全,阅读专题下面的文章了解更多详细内容。

68

2026.02.11

Yandex网页版官方入口使用指南_国际版与俄罗斯版访问方法解析
Yandex网页版官方入口使用指南_国际版与俄罗斯版访问方法解析

本专题全面整理了Yandex搜索引擎的官方入口信息,涵盖国际版与俄罗斯版官网访问方式、网页版直达入口及免登录使用说明,帮助用户快速、安全地进入Yandex官网,高效使用其搜索与相关服务。

200

2026.02.11

虫虫漫画网页版入口与免费阅读指南_正版漫画全集在线查看方法
虫虫漫画网页版入口与免费阅读指南_正版漫画全集在线查看方法

本专题系统整理了虫虫漫画官网及网页版最新入口,涵盖免登录观看、正版漫画全集在线阅读方式,并汇总稳定可用的访问渠道,帮助用户快速找到虫虫漫画官方页面,轻松在线阅读各类热门漫画内容。

40

2026.02.11

Docker容器化部署与DevOps实践
Docker容器化部署与DevOps实践

本专题面向后端与运维开发者,系统讲解 Docker 容器化技术在实际项目中的应用。内容涵盖 Docker 镜像构建、容器运行机制、Docker Compose 多服务编排,以及在 DevOps 流程中的持续集成与持续部署实践。通过真实场景演示,帮助开发者实现应用的快速部署、环境一致性与运维自动化。

4

2026.02.11

Rust异步编程与Tokio运行时实战
Rust异步编程与Tokio运行时实战

本专题聚焦 Rust 语言的异步编程模型,深入讲解 async/await 机制与 Tokio 运行时的核心原理。内容包括异步任务调度、Future 执行模型、并发安全、网络 IO 编程以及高并发场景下的性能优化。通过实战示例,帮助开发者使用 Rust 构建高性能、低延迟的后端服务与网络应用。

1

2026.02.11

Spring Boot企业级开发与MyBatis Plus实战
Spring Boot企业级开发与MyBatis Plus实战

本专题面向 Java 后端开发者,系统讲解如何基于 Spring Boot 与 MyBatis Plus 构建高效、规范的企业级应用。内容涵盖项目架构设计、数据访问层封装、通用 CRUD 实现、分页与条件查询、代码生成器以及常见性能优化方案。通过完整实战案例,帮助开发者提升后端开发效率,减少重复代码,快速交付稳定可维护的业务系统。

6

2026.02.11

包子漫画网页版入口与全集阅读指南_正版免费漫画快速访问方法
包子漫画网页版入口与全集阅读指南_正版免费漫画快速访问方法

本专题汇总了包子漫画官网和网页版入口,提供最新章节抢先看方法、正版免费阅读指南,以及稳定访问方式,帮助用户快速直达包子漫画页面,无广告畅享全集漫画内容。

159

2026.02.10

MC.JS网页版快速畅玩指南_MC.JS官网在线入口及免安装体验方法
MC.JS网页版快速畅玩指南_MC.JS官网在线入口及免安装体验方法

本专题汇总了MC.JS官网入口和网页版快速畅玩方法,提供免安装访问、不同版本(1.8.8、1.12.8)在线体验指南,以及正版网页端操作说明,帮助玩家轻松进入MC.JS世界,实现即时畅玩与高效体验。

89

2026.02.10

谷歌邮箱网页版登录与注册全指南_Gmail账号快速访问与安全操作教程
谷歌邮箱网页版登录与注册全指南_Gmail账号快速访问与安全操作教程

本专题汇总了谷歌邮箱网页版的最新登录入口和注册方法,详细提供官方账号快速访问方式、网页版操作教程及安全登录技巧,帮助用户轻松管理Gmail邮箱账户,实现高效、安全的邮箱使用体验。

78

2026.02.10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP数据库编程-MySQLi/PDO
PHP数据库编程-MySQLi/PDO

共11课时 | 1万人学习

MySQLi 扩展库视频教程
MySQLi 扩展库视频教程

共24课时 | 5.6万人学习

MySQLi面向对象编程极速入门
MySQLi面向对象编程极速入门

共10课时 | 7.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号