PHP实现MySQL并发查询-阿里云开发者社区

开发者社区> 速贷100> 正文

PHP实现MySQL并发查询

简介: 一般的,一个看似很简单的页面,一次http请求后,到达服务端,穿过Cache层,落到后台后,实际可能会有很多很多的数据查询逻辑!而这些查询实际是不相互依赖的,也即可以同时查询。比如各种用户信息,用户的APP列表,每个APP对应的流量数据、消耗记录、服务状态,平台运行状态,消息通知,新闻资讯等等。
+关注继续查看
一般的,一个看似很简单的页面,一次http请求后,到达服务端,穿过Cache层,落到后台后,实际可能会有很多很多的数据查询逻辑!而这些查询实际是不相互依赖的,也即可以同时查询。比如各种用户信息,用户的APP列表,每个APP对应的流量数据、消耗记录、服务状态,平台运行状态,消息通知,新闻资讯等等。
这篇文章主要介绍了数据查询层,如何把串行变并行,提高查询效率、提升应用性能。实现方式包括:mysqlnd异步查询,cURL并发请求,Swoole异步非阻塞!

PHP脚本是按文档流的形式来执行的,所以我们在编写PHP程序时,代码基本都是串行的,尤其是SQL,比如:
<ignore_js_op> 
这种方式,是每次查询都需要等待结果返回之后再开始下一次的查询,
运行时间 = (第1次发送请求时间 + 0.01 + 第1次返回时间)+(第2次发送请求时间 + 0.05 + 第2次返回时间)+(第3次发送请求时间 + 0.03 + 第3次返回时间)

如果是并发查询,那么流程就成了:
<ignore_js_op> 
运行时间 = 发送请求时间 + 0.05 + 返回时间

显然,并发查询要比串行查询快!

那么PHP可以实现并发查询吗?答案是肯定的!

一、利用MySQL的异步查询功能

目前 MySQL 的异步查询,只在 MySQLi 扩展提供,查询方法分别是:

  • 使用 MYSQLI_ASYNC 模式执行 mysqli::query
  • 获取异步查询结果:mysqli::reap_async_query


需要注意的是,使用异步查询,需要使用 mysqlnd 作为PHP的MySQL数据库驱动,
mysqlnd(MySQL Native Driver) 是 Zend 公司开发的 MySQL 数据库驱动,采用PHP开源协议,用于代替旧版的由 MySQL AB公司(现在的Oracle)开发的 libmysql,PHP 5.3 及以上版本开始提供,PHP5.4 之后的版本 mysqlnd 为默认配置选项,
如果 PHP 小于 5.4,编译时需要指定编译参数:

  1. --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd
复制代码

MySQL异步查询示例脚本:

  1. /**
  2. * MySQL异步查询示例脚本:
  3. * @filename p_async.php
  4. * @url http://test.979137.com/ParallelSQL/p_async.php
  5. */
  6. //期望结果集:获取以下用户的每个月每个APP的消费统计
  7. $top = array('979137', '555555', '666666', '888888', '999999');
  8. $ret = array_fill_keys($top, array());
  9. //组织结构查询
  10. $cmd = $resources = array();
  11. $sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_2016%s WHERE uid=%d AND product='SAE' GROUP BY access_key"; 
  12. foreach($top as $uid) {
  13.     for($i = 1; $i <= 12; $i++) {
  14.         $ret[$uid][$i] = $tmp = array();
  15.         $tmp['uid'] = $uid;
  16.         $tmp['month'] = $i; 
  17.         $tmp['resource'] = $resources[] = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
  18.         $tmp['resource']->set_charset('utf8');
  19.         $tmp['resource']->query(sprintf($sql, sprintf('%02d', $i), $uid), MYSQLI_ASYNC);
  20.         $tag = spl_object_hash($tmp['resource']);
  21.         $cmd[$tag] = $tmp;
  22.     }   
  23. }
  24. $total = $query_times = count($resources);
  25. //获取结果
  26. do {
  27.     $read = $error = $reject = $resources;
  28.     //等待查询结束
  29.     if (!\mysqli::poll($read, $error, $reject, 1)) {
  30.         continue;
  31.     }   
  32.     //批量获取结果
  33.     foreach($read as $resource) {
  34.         $result = $resource->reap_async_query();
  35.         if ($result) {
  36.             $tag = spl_object_hash($resource);
  37.             $uid = $cmd[$tag]['uid'];
  38.             $month = $cmd[$tag]['month'];
  39.             while(($row = $result->fetch_assoc()) != false) {
  40.                 $ret[$uid][$month][$row['access_key']] = $row['sum_amount'];
  41.             }   
  42.             $result->free();
  43.             $total--;
  44.     
  45.         } else die('MySQLi error: '.$resource->error);
  46.     }   
  47. } while ($total > 0);
  48. var_dump($ret);
复制代码



二、cURL实现并发请求

先解释下 cURL 和 SQL 怎么就扯上关系了呢!

我们知道在很多系统架构里,PHP是不会直接操作DB的,而是 RESTFull 架构,这时候所有操作都接口化了,
这时上述所讲的 SQL 就演变成 接口调用 了,
因为 API,所以 cURL!

以下是PHP中cURL多线程相关函数:

curl_multi_add_handle — 向curl批处理会话中添加单独的curl句柄
curl_multi_close — 关闭一组cURL句柄
curl_multi_exec — 运行当前 cURL 句柄的子连接
curl_multi_getcontent — 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
curl_multi_info_read — 获取当前解析的cURL的相关传输信息
curl_multi_init — 返回一个新cURL批处理句柄
curl_multi_remove_handle — 移除curl批处理句柄资源中的某个句柄资源
curl_multi_select — 等待所有cURL批处理中的活动连接
curl_multi_setopt — 为 cURL 并行处理设置一个选项
curl_multi_strerror —  返回描述错误码的字符串文本


我们可以利用这些多线程函数,实现 cURL 并发请求,从而实现并发 SQL!

cURL并发请求,服务端接口示例脚本:

  1. /**
  2. * cURL并发请求,服务端接口示例脚本
  3. * @filename consume.php
  4. * @url http://test.979137.com/ParallelSQL/consume.php
  5. */
  6. $resource = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
  7. $resource->set_charset('utf8');
  8. $sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_%d WHERE uid=%d AND product='%s' GROUP BY access_key";
  9. $sql = sprintf($sql, $_GET['ym'], $_GET['uid'], $_GET['product']);
  10. $res = $resource->query($sql);
  11. $out['code'] = $resource->errno;
  12. $out['message'] = $resource->error;
  13. $data = array();
  14. if (is_object($res)) {
  15.     while(($row = $res->fetch_assoc()) != false) {
  16.         $data[$row['access_key']] = $row['sum_amount'];
  17.     }   
  18. }
  19. $out['data'] = $data;
  20. header('Content-Type: application/json; charset=utf-8');
  21. echo json_encode($out);
复制代码



cURL并发请求,客户端调用示例脚本:

  1. /**
  2. * cURL并发请求,客户端调用示例脚本
  3. * @filename p_curl.php
  4. * @url http://test.979137.com/ParallelSQL/p_curl.php
  5. */
  6. //期望结果集:获取以下用户的每个月每个APP的消费统计
  7. $top = array('979137', '555555', '666666', '888888', '999999');
  8. $ret = array_fill_keys($top, array());
  9. $mch = curl_multi_init();
  10. $opt[CURLOPT_HEADER] = 0;
  11. $opt[CURLOPT_CONNECTTIMEOUT] = 60; 
  12. $opt[CURLOPT_RETURNTRANSFER] = true;
  13. $opt[CURLOPT_HTTPHEADER] = array('Host: api.979137.com');
  14. //生成句柄并加入到批处理
  15. $cmd = array();
  16. foreach($top as $uid) {
  17.     for($i = 1; $i <= 12; $i++) {
  18.         $ret[$uid][$i] = $tmp = array();
  19.         $tmp['url'] = sprintf('http://127.0.0.1/ParallelSQL/consume.php?ym=2016%02d&uid=%d&product=SAE', $i, $uid);
  20.         $tmp['uid'] = $uid;
  21.         $tmp['month'] = $i; 
  22.         $tmp['resource'] = curl_init($tmp['url']);   
  23.         curl_setopt_array($tmp['resource'], $opt);
  24.         curl_multi_add_handle($mch, $tmp['resource']);
  25.         $cmd[] = $tmp;
  26.     }   
  27. }
  28. //并发执行,直到全部结束。
  29. do {
  30.     curl_multi_exec($mch, $active);   
  31. } while ($active > 0); 
  32. //获取全部结果
  33. foreach($cmd as $c) {
  34.     $res = curl_multi_getcontent($c['resource']);
  35.     $http_code = curl_getinfo($c['resource'], CURLINFO_HTTP_CODE);
  36.     if ($res === false || $http_code != 200) {
  37.         die(curl_error($c['resource']));
  38.     }   
  39.     $res = json_decode($res, true);
  40.     $res['code'] && die($res['message']);
  41.     $ret[$c['uid']][$c['month']] = $res['data'];
  42. }
  43. curl_multi_close($mch);
  44. var_dump($ret);
复制代码



查询效率对比
1、数据表机构:

  1. CREATE TABLE `consume_201612` (
  2.   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  3.   `uid` int(10) unsigned NOT NULL,
  4.   `product` enum('UID','SAE','SC2','SCE','SEM','SCS','SLS') NOT NULL DEFAULT 'SAE',
  5.   `access_key` varchar(255) NOT NULL,
  6.   `service_code` varchar(255) NOT NULL,
  7.   `amount` int(10) unsigned NOT NULL,
  8.   `remark` varchar(255) NOT NULL,
  9.   `data` text NOT NULL,
  10.   `times` int(10) unsigned NOT NULL,
  11.   PRIMARY KEY (`id`),
  12.   KEY `uid_pa` (`uid`,`product`,`access_key`)
  13. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
复制代码

2、数据量:总共分12张表,每张表的数量在 500万~1100万 之间,全量数据在1亿以上

3、MySQL异步查询结果:

$ mysql.server restart
$ php p_async.php 
Query times: 60
Run time: 2.4453s
(CPU)User time: 0.087966s
(CPU)System time: 0.117625s

4、cURL并发请求查询结果

$ mysql.server restart
$ php p_curl.php
Query times: 60
Run time: 2.173035s
(CPU)User time: 0.40652s
(CPU)System time: 0.869902s

5、普通串行查询结果

$ mysql.server restart
$ php p_sync.php 
Query times: 60
Run time: 20.485623s
(CPU)User time: 0.083185s
(CPU)System time: 0.036566s
Memory usage: 304.72kb

并发执行时,我们可以在 MySQL 服务器看到所有的正在执行的SQL:
<ignore_js_op>

WechatIMG81.jpeg (690.09 KB, 下载次数: 0)

下载附件

2016-12-6 18:51 上传

 



总结

1、在并发查询下,查询效率提高了近10倍
2、使用 MySQL 异步查询,因为需要给所有查询都创建一个新的连接,而 MySQL 服务端会为每个连接创建一个单独的线程进行处理,如果创建的线程数过多,会给系统造成负担,请谨慎使用
3、使用 cURL 并发请求后端接口时,CPU负载明显上升,所以并发请求后端接口,一定程度上会增加后端压力,这和前端大流量下的高并发原理是一样的
4、使用 cURL 并发请求,还需要考虑一个网络延时的问题,网络延时越小,查询效率提升越明显。如果你是想代替类方法或函数调用,在条件允许的情况,建议直接连接服务器本机即127.0.0.1
5、在并发请求下,因为需要一次性接收全部返回结果,所以会占用更多的内存资源

需要说明的是,在实际应用中 cURL 的并发请求,一般不只单用于数据查询,而是为了完成更多的后台业务逻辑,
所以,在服务器负载能力允许的情况下,推荐使用 cURL 并行转发的形式,提升前端响应速度!


最后说一下 Swoole,

Swoole 是 PHP 的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询!
其中的异步MySQL,其原理是通过 MYSQLI_ASYNC 模式查询,然后获取 mysql 连接的 socket,加入到 epoll 时间循环中,当数据库返回结果时会回调指定函数。
这个过程是完全异步非阻塞的,不浪费CPU,具体实现方式这里不再详细介绍!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10051 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
2958 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
11595 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10882 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13865 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
11887 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
7458 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
4497 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
7359 0
+关注
28
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载