PHP API中,MYSQL與MYSQLI的持久鏈接區別

留存再測試驗證php


PHP API中,MYSQL與MYSQLI的持久鏈接區...mysql

好久好久之前,我也是由於工做上的bug,研究了php mysql client的鏈接驅動mysqlnd 與libmysql之間的區別php與mysql通信那點事,此次又遇到一件跟他們有聯繫的事情,mysqli與mysql持久連接的區別。寫出這篇文章,用了好一個多月,其一是我太懶了,其二是工做也比較忙。最近才能騰出時間,來作這些事情。每次作總結,都要認真閱讀源碼,理解含義,測試驗證,來確認這些細節。而每個步驟都須要花費很長的時間,並且,還不能被打斷。一旦被打斷了,都須要很長時間去溫習上下文。也故意強迫本身寫這篇總結,改改本身的惰性。
在我和個人小夥伴們如火如荼的開發、測試時發生了「mysql server too many connections」的錯誤,稍微排查了一下,發現是php後臺進程創建了大量的連接,而沒有關閉。服務器環境大約以下php5.3.x 、mysqli API、mysqlnd 驅動。代碼狀況是這樣:sql

//後臺進程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); //注意這裏,哪裏不舒服,就注意看哪裏。windows

$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方法是這樣寫的api

//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類的代碼session

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

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部分代碼
tcp

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);
}
}
}

從上面的代碼能夠看到,咱們已經啓用長連接了啊,爲什麼頻繁創建了這麼多連接呢?爲了模擬重現這個問題,我在本地開發環境進行測試,不管如何也重現不了,對比了下環境,個人開發環境是windows七、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)) { //(讓你看時,你再看)注意看這裏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底層實現,實際上也是這個緣由。 代碼以下:

<?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: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 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; }} 看來,開發環境跟運行環境一致是多麼的重要,不然就不會遇到這些問題了。不過,若是沒遇到這麼有意思的問題,豈不是太惋惜了

相關文章
相關標籤/搜索