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

很久很久以前,我也是因为工作上的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部分代码

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”)

//文件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)) {    //(让你看时,