一般的,一个看似很简单的页面,一次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. } else die('MySQLi error: '.$resource->error);
  45. }
  46. } while ($total > 0);
  47. 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,具体实现方式这里不再详细介绍!

PHP实现MySQL并发查询的更多相关文章

  1. mysql 处理查询请求过程

    需要搞清楚查询为什么会慢,就要搞清楚mysql处理查询请求的过程: 1.客户端发送SQL请求给服务器 2.服务器检查是否可以在查询缓存中命中该SQL   查询缓存对SQL性能的影响. 1.需要对缓存加 ...

  2. MySQL慢查询(二) - pt-query-digest详解慢查询日志

    一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...

  3. 深入mysql慢查询设置的详解

    set long_query_time=1; #设置慢查询时间为1 秒; set global slow_query_log=on; #开启慢查询日志; show global status like ...

  4. Linux 实例如何开启 MySQL 慢查询功能

    运行 MySQL 时,查询速度比较慢的语句对数据库的影响非常大,这些慢语句大多是写的不够合理或者大数据环境下多表并发查询造成的.MySQL 自带慢查询功能,能记录查询时间超过参数 long_query ...

  5. mysql慢查询----pt-query-digest详解慢查询日志(linux系统)

    一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...

  6. MySQL简单查询详解-单表查询

    MySQL简单查询详解-单表查询 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.查询的执行路径 一条SQL查询语句的执行过程大致如下图所示: 1>.客户端和服务端通过my ...

  7. mysql慢查询和php-fpm慢日志

    MySQL慢查询 在web开发中,我们经常会写出一些SQL语句,一条糟糕的SQL语句可能让你的整个程序都非常慢,超过10秒一般用户就会选择关闭网页,如何优化SQL语句将那些运行时间 比较长的SQL语句 ...

  8. mysql慢查询开启及分析方法

    最近服务维护的公司的DB服务器,总是会出现问题,感觉需要优化一下了,登陆上去,发现慢查询日志都没有开,真是惭愧, 故果断加上慢查询日志, 经过分析sql记录,发现问题很多,开发人员很多没有对sql优化 ...

  9. MySQL慢查询分析工具pt-query-digest详解

    一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...

随机推荐

  1. springboot系列(三)配置文件详解

    目录 properties 文件 1.语法 2.优先级 3.自定义数据配置 4.获取自定义数据配置 1.通过prefix获取 yml文件 1.语法 2.优先级 3.自定义数据配置. 4.获取自定义数据 ...

  2. Mac OS修改VSCode Go的默认缩进格式

    一.在VSCode中编写Go代码时,缩进是使用tab缩进,主要是由于以下两个方面. 1. Go官方提供的代码格式化工具gofmt默认是使用tab缩进,并且为8个字符宽度. 2. 并且在VSCode中, ...

  3. python 报错:a bytes-like object is required, not 'str'

    核心代码: def ipPools(numPage): headers = randomHeads() url = 'http://www.xicidaili.com/nn/' saveFsvFile ...

  4. B 蒜头君的树

    时间限制 : - MS   空间限制 : - KB  评测说明 : 2s,256m 问题描述 蒜头君有一棵有根树,树的每一边都有边权,蒜头君想知道任意两点间最短距离之和为多少. 另外,由于各种原因,蒜 ...

  5. L - Subway(最短路spfa)

    L - Subway(最短路spfa) You have just moved from a quiet Waterloo neighbourhood to a big, noisy city. In ...

  6. python实现杨辉三角形

    代码实现: # python实现杨辉三角形 def yanghui(): # 定义第一行列表为[1] line = [1] while True: # yield的作用:把一个函数变成生成器,同时返回 ...

  7. jq日历一周为单位轮播

    简单效果图: 代码如下: <!doctype html> <html lang="en"> <head> <meta charset=&q ...

  8. pythone 时间模块

    时间模块(时区) 计算方式:时间戳是一串数字,从计算机诞生的那一秒到现在过了多少秒,每过一秒+1 #时间戳#由时间戳获取格式化时间#由格式化时间获取时间戳 import time def timene ...

  9. word多级列表应用

  10. Docker命名空间

    命名空间 命名空间( namespace )是 Linux 内核的一个强大特性,为容器虚拟化的实现带来极大便利,利用这 特性,每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系 ...