PHP程序如何通過協(xié)程來實(shí)現(xiàn)mysql查詢異步化
寫在文章之初
最近看了很多PHP大牛們的視頻,了解到facebook的mysql查詢可以進(jìn)行異步化,從而提高性能。由于facebook實(shí)現(xiàn)的比較早,他們不得不對php進(jìn)行hack才得以實(shí)現(xiàn)?,F(xiàn)在的php7.0,已經(jīng)無需hack就可以實(shí)現(xiàn)了。
對于一個(gè)web網(wǎng)站的性能來說,瓶頸多半是來自于數(shù)據(jù)庫。一般數(shù)據(jù)庫查詢會(huì)在某個(gè)請求的整體耗時(shí)中占很大比例。如果能提高數(shù)據(jù)庫查詢的效率,網(wǎng)站的整體響應(yīng)時(shí)間會(huì)有很大的下降。如果能實(shí)現(xiàn)mysql查詢的異步化,就可以實(shí)現(xiàn)多條sql語句同時(shí)執(zhí)行。這樣就可以大大縮短mysql查詢的耗時(shí)。
so問題來了,php程序異步執(zhí)行為什么比同步塊?
與異步查詢相反的時(shí)同步查詢。通常情況下mysql的query查詢都是同步方式。下面我們對兩種方式做下對比。對比的例子是,請求兩次select sleep(1)。這條語句在mysql服務(wù)器端大概耗時(shí)1000ms。
下面是程序同步方式的執(zhí)行流程圖:
上面的流程圖,解釋一下為:
第一步,向mysql服務(wù)器端發(fā)送第一次查詢請求。大概耗時(shí) 1ms
第二步,mysql服務(wù)器端返回第一次查詢的結(jié)果。大概耗時(shí) 1000ms
第三步,向mysql服務(wù)器再次發(fā)送請求。大概耗時(shí) 1ms
第四步,mysql服務(wù)器端返回第二次查詢的結(jié)果。大概耗時(shí) 1000ms
同步的方式執(zhí)行兩次select sleep(1),大概耗時(shí) 2002ms。
而異步方式的執(zhí)行流程圖如下:
我們隊(duì)異步方式執(zhí)行進(jìn)行解釋:
第一步,向mysql服務(wù)器端發(fā)送第一次查詢請求。大概耗時(shí)1ms
第二步,在等待第一次請求返回?cái)?shù)據(jù)的同時(shí),向服務(wù)器端發(fā)送第二次查詢請求。大概耗時(shí) 1ms
第三步,接受mysql服務(wù)器端返回的兩次查詢請求。大概耗時(shí) 1000ms。
對兩次分析對比,我們可以得出:
異步查詢比同步查詢速度快,是因?yàn)槎鄺l查詢語句在服務(wù)器端同時(shí)執(zhí)行,大大縮短了服務(wù)器端的響應(yīng)時(shí)間。并行一般情況下總比串行快嘛。sql語句執(zhí)行時(shí)間越長,效果越明顯。
so,問題又來了,我們該如何實(shí)現(xiàn)mysql的異步查詢?
要實(shí)現(xiàn)異步查詢的關(guān)鍵是能把發(fā)送請求和接受返回?cái)?shù)據(jù)分開。正好mysqlnd中提供了這個(gè)特性。
在mysqlnd中對應(yīng)的方法是:
mysqlnd_async_query 發(fā)送查詢請求
mysqlnd_reap_async_query 獲取查詢結(jié)果
mysqli擴(kuò)展針對mysqlnd的這個(gè)特性做了封裝,在調(diào)用query方法時(shí),傳入MYSQLI_ASYNC即可。
那么,為什么要使用協(xié)程?
查看了我們推薦文章的代碼實(shí)現(xiàn),是不是感覺寫法和平時(shí)不一樣?一般在項(xiàng)目當(dāng)中,我們都是以function的形式去相互調(diào)用,function中包含了數(shù)據(jù)庫查詢。為了保持這個(gè)習(xí)慣,方便大家使用,因此引入了協(xié)程。在php5.5中正好提供了yield和generator,方便我們實(shí)現(xiàn)協(xié)程。示例代碼如下:
<?php function f1(){ $db = new db(); $obj = $db->async_query('select sleep(1)'); echo "f1 async_query \n"; yield $obj; $row = $db->fetch(); echo "f1 fetch\n"; yield $row; } function f2(){ $db = new db(); $obj = $db->async_query('select sleep(1)'); echo "f2 async_query\n"; yield $obj; $row = $db->fetch(); echo "f2 fetch\n"; yield $row; } $gen1 = f1(); $gen2 = f2(); $gen1->current(); $gen2->current(); $gen1->next(); $gen2->next(); $ret1 = $gen1->current(); $ret2 = $gen2->current(); var_dump($ret1); var_dump($ret2); class db{ static $links; private $obj; function getConn(){ $host = '127.0.0.1'; $user = 'demo'; $password = 'demo'; $database = 'demo'; $this->obj = new mysqli($host, $user, $password, $database); self::$links[spl_object_hash($this->obj)] = $this->obj; return self::$links[spl_object_hash($this->obj)]; } function async_query($sql){ $link = $this->getConn(); $link->query($sql, MYSQLI_ASYNC); return $link; } function fetch(){ for($i = 1; $i <= 5; $i++){ $read = $errors = $reject = self::$links; $re = mysqli_poll($read, $errors, $reject, 1); foreach($read as $obj){ if($this->obj === $obj){ $sql_result = $obj->reap_async_query(); $sql_result_array = $sql_result->fetch_array(MYSQLI_ASSOC);//只有一行 $sql_result->free(); return $sql_result_array; } } } } } ?>
在終端命令行方式執(zhí)行結(jié)果如下:
$time php ./async.php f1 async_query f2 async_query f1 fetch f2 fetch array(1) { ["sleep(1)"]=> string(1) "0" } array(1) { ["sleep(1)"]=> string(1) "0" } real 0m1.016s user 0m0.007s
從結(jié)果上我們可以看出執(zhí)行流程是,先發(fā)了兩次mysql查詢,然后在接受數(shù)據(jù)庫的返回?cái)?shù)據(jù)。正常情況下,至少需要2000ms才能執(zhí)行完畢。但是,real 0m1.016s,說明兩次查詢的耗時(shí)只有1016ms。
tips:以上代碼只是示例代碼,還有一些需要完善的地方。
要注意的地方:
需要注意的是,如果mysql服務(wù)器本身負(fù)載很大,這種并行執(zhí)行的方式就不一定是好的解決方法。因?yàn)椋琺ysql服務(wù)端會(huì)為每個(gè)鏈接創(chuàng)建一個(gè)單獨(dú)的線程進(jìn)行處理。如果創(chuàng)建的線程數(shù)過多,會(huì)給系統(tǒng)造成負(fù)擔(dān)。因此,最好的解決需求的方式是因地制宜,根據(jù)現(xiàn)實(shí)情況制定出最有利當(dāng)前狀況的方案才是上佳之選,不一定某一種模式更好于另一種模式。本文向大家提供的是一種選擇。
網(wǎng)站建設(shè)首選石家莊尚途網(wǎng)絡(luò)科技有限公司,更多網(wǎng)站優(yōu)化,網(wǎng)站建設(shè)信息請關(guān)注:尚途科技,網(wǎng)址:http://jbbow.cn
- 上一條: 教你如何自學(xué)PHP擴(kuò)展
- 下一條: php防止mysql注入