PHP API中,MYSQL與MYSQLI的持久串連區別

來源:互聯網
上載者:User
很久很久以前,我也是因為工作上的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還是truestatic 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)) {    //(讓你看時,你再看)注意看這裡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);#endifzend_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底層實現,實際上也是這個原因。 代碼如下:


<?php$links = array();for ($i = 0; $i < 15; $i++) {    $links[] =  mysqli_connect('p:192.168.1.40', 'USER', 'PWD', 'DB', 3306);}sleep(15);
 

查看進程列表裡是這樣的結果:


netstat -an  grep 192.168.1.40:3306tcp        0      0 192.168.1.6:52441       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52454       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52445       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52443       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52446       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52449       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52452       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52442       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52450       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52448       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52440       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52447       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52444       192.168.1.40:3306       ESTABLISHEDtcp        0      0 192.168.1.6:52451       192.168.1.40:3306       ESTABLISHEDtcp        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 FACTORYclass 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 adapterclass 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;    }}
 

看來,開發環境跟運行環境一致是多麼的重要,否則就不會遇到這些問題了。不過,如果沒遇到這麼有意思的問題,豈不是太可惜了




相關文章

Beyond APAC's No.1 Cloud

19.6% IaaS Market Share in Asia Pacific - Gartner IT Service report, 2018

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。