ホームページ php教程 php手册 PHP API における、MYSQL と MYSQLI の間の永続的接続の違い

PHP API における、MYSQL と MYSQLI の間の永続的接続の違い

Jun 21, 2016 am 08:48 AM
gt mysql mysqli

ずっと昔、仕事上のバグのため、PHP mysql クライアントの接続ドライバー mysqlnd と libmysql の違いと、PHP と mysql 間の通信について研究しました。今回は、それらに関連する別のこと、mysqli に遭遇しました。 mysql 永続リンクとの違い。第一に、私はとても怠け者であり、第二に、仕事がとても忙しいです。こういったことをする時間を見つけたのはつい最近のことです。まとめを作成するたびに、ソースコードを注意深く読み、意味を理解し、それを確認するためにテストと検証を行う必要があります。各ステップには長い時間がかかり、中断することはできません。一度中断されると、コンテキストを確認するのに長い時間がかかります。私も自分の怠け心を変えるために、わざと自分にこのまとめを書かせました。

友人と私が本格的に開発とテストを行っていたときに、「mysql サーバーの接続が多すぎます」エラーが発生しました。少しトラブルシューティングを行った結果、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 メソッドはシングルトンのみをシミュレートし、新しいオブジェクトを作成して返し、それらはすべて Afx_Module_Abstract クラスを継承していることがわかります。 Afx_Module_Abstract クラスのおおよそのコードは次のとおりです:


abstract class Afx_Module_Abstract
{
    public function setAdapter ($_adapter)
    {
        $this->_adapter = $_adapter;
    }
}
ログイン後にコピー

Afx_Module_Abstract クラスのキーコードは上記の通りです。 DB に関しては、「バックグラウンドプロセス A」に戻る setAdapter メソッドが 1 つだけあり、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 アダプター部分を見てください。


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 でした。サーバー上の API と矛盾していました。この問題は、mysql と mysqli、または 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 サーバーとの TCP リンクを確立すると、そのリンクが直ちにpersistent_list に保存され、次回リンクが確立されるときに最初にチェックされることがわかります。 IP、PORT、USER、PASS、CLIENT_FLAGS のリンクが存在する場合はそれを使用し、存在しない場合は新しいリンクを作成します。

PHP の mysqli 拡張では、リンクを保存するためにpersistent_list が使用されるだけでなく、現在アイドル状態の TCP リンクを保存するために free_link も使用されます。検索時には、無料の free_link リンクリストに存在するかどうかも判断され、存在する場合にのみ TCP リンクが使用されます。 mysqli_closez または RSHUTDOWN の後、このリンクは 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 は、C API の mysql_change_user を呼び出して、一時セッション変数をクリーンアップします。現在の TCP リンクは不完全です。コミット ロールバック命令、テーブルのロック命令、一時テーブルのロック解除などを記述します (これらの命令はすべて、送信された SQL 命令を判断して応答を決定する PHP の mysqli ではなく、mysql サーバー自体によって決定されます)。 )、マニュアル『mysqli 拡張機能と永続接続』の手順を参照してください。この設計はこの新機能用であり、mysql 拡張機能はこの機能をサポートしていません。

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


<?php
$links = array();
for ($i = 0; $i < 15; $i++) {
    $links[] =  mysqli_connect(&#39;p:192.168.1.40&#39;, &#39;USER&#39;, &#39;PWD&#39;, &#39;DB&#39;, 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(&#39;p:192.168.1.40&#39;, &#39;USER&#39;, &#39;PWD&#39;, &#39;DB&#39;, 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 = &#39;mysql&#39;;
    const DB_MYSQLI = &#39;mysqli&#39;;
    const DB_PDO = &#39;pdo&#39;;

    static $drivers = array(
        &#39;mysql&#39;=>array(),&#39;mysqli&#39;=>array(),&#39;pdo&#39;=>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;
    }
}
ログイン後にコピー

 

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




このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

PHPのビッグデータ構造処理スキル PHPのビッグデータ構造処理スキル May 08, 2024 am 10:24 AM

ビッグ データ構造の処理スキル: チャンキング: データ セットを分割してチャンクに処理し、メモリ消費を削減します。ジェネレーター: データ セット全体をロードせずにデータ項目を 1 つずつ生成します。無制限のデータ セットに適しています。ストリーミング: ファイルやクエリ結果を 1 行ずつ読み取ります。大きなファイルやリモート データに適しています。外部ストレージ: 非常に大規模なデータ セットの場合は、データをデータベースまたは NoSQL に保存します。

PHP で MySQL のバックアップと復元を使用するにはどうすればよいですか? PHP で MySQL のバックアップと復元を使用するにはどうすればよいですか? Jun 03, 2024 pm 12:19 PM

PHP で MySQL データベースをバックアップおよび復元するには、次の手順を実行します。 データベースをバックアップします。 mysqldump コマンドを使用して、データベースを SQL ファイルにダンプします。データベースの復元: mysql コマンドを使用して、SQL ファイルからデータベースを復元します。

PHP で MySQL クエリのパフォーマンスを最適化するにはどうすればよいですか? PHP で MySQL クエリのパフォーマンスを最適化するにはどうすればよいですか? Jun 03, 2024 pm 08:11 PM

MySQL クエリのパフォーマンスは、検索時間を線形の複雑さから対数の複雑さまで短縮するインデックスを構築することで最適化できます。 PreparedStatement を使用して SQL インジェクションを防止し、クエリのパフォーマンスを向上させます。クエリ結果を制限し、サーバーによって処理されるデータ量を削減します。適切な結合タイプの使用、インデックスの作成、サブクエリの使用の検討など、結合クエリを最適化します。クエリを分析してボトルネックを特定し、キャッシュを使用してデータベースの負荷を軽減し、オーバーヘッドを最小限に抑えます。

PHP を使用して MySQL テーブルにデータを挿入するにはどうすればよいですか? PHP を使用して MySQL テーブルにデータを挿入するにはどうすればよいですか? Jun 02, 2024 pm 02:26 PM

MySQLテーブルにデータを挿入するにはどうすればよいですか?データベースに接続する: mysqli を使用してデータベースへの接続を確立します。 SQL クエリを準備します。挿入する列と値を指定する INSERT ステートメントを作成します。クエリの実行: query() メソッドを使用して挿入クエリを実行します。成功すると、確認メッセージが出力されます。

PHP を使用して MySQL テーブルを作成するにはどうすればよいですか? PHP を使用して MySQL テーブルを作成するにはどうすればよいですか? Jun 04, 2024 pm 01:57 PM

PHP を使用して MySQL テーブルを作成するには、次の手順が必要です。 データベースに接続します。データベースが存在しない場合は作成します。データベースを選択します。テーブルを作成します。クエリを実行します。接続を閉じます。

PHP で MySQL ストアド プロシージャを使用するにはどうすればよいですか? PHP で MySQL ストアド プロシージャを使用するにはどうすればよいですか? Jun 02, 2024 pm 02:13 PM

PHP で MySQL ストアド プロシージャを使用するには: PDO または MySQLi 拡張機能を使用して、MySQL データベースに接続します。ストアド プロシージャを呼び出すステートメントを準備します。ストアド プロシージャを実行します。結果セットを処理します (ストアド プロシージャが結果を返す場合)。データベース接続を閉じます。

MySQL 8.4 で mysql_native_password がロードされていないエラーを修正する方法 MySQL 8.4 で mysql_native_password がロードされていないエラーを修正する方法 Dec 09, 2024 am 11:42 AM

MySQL 8.4 (2024 年時点の最新の LTS リリース) で導入された主な変更の 1 つは、「MySQL Native Password」プラグインがデフォルトで有効ではなくなったことです。さらに、MySQL 9.0 ではこのプラグインが完全に削除されています。 この変更は PHP および他のアプリに影響します

PHP で MySQLi を使用してデータベース接続を確立するための詳細なチュートリアル PHP で MySQLi を使用してデータベース接続を確立するための詳細なチュートリアル Jun 04, 2024 pm 01:42 PM

MySQLi を使用して PHP でデータベース接続を確立する方法: MySQLi 拡張機能を含める (require_once) 接続関数を作成する (functionconnect_to_db) 接続関数を呼び出す ($conn=connect_to_db()) クエリを実行する ($result=$conn->query()) 閉じる接続 ( $conn->close())

See all articles