PHP实现MySQL并发查询
一般的,一个看似很简单的页面,一次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,编译时需要指定编译参数:
- --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd
复制代码
MySQL异步查询示例脚本:
- /**
- * MySQL异步查询示例脚本:
- * @filename p_async.php
- * @url http://test.979137.com/ParallelSQL/p_async.php
- */
- //期望结果集:获取以下用户的每个月每个APP的消费统计
- $top = array('979137', '555555', '666666', '888888', '999999');
- $ret = array_fill_keys($top, array());
- //组织结构查询
- $cmd = $resources = array();
- $sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_2016%s WHERE uid=%d AND product='SAE' GROUP BY access_key";
- foreach($top as $uid) {
- for($i = 1; $i <= 12; $i++) {
- $ret[$uid][$i] = $tmp = array();
- $tmp['uid'] = $uid;
- $tmp['month'] = $i;
- $tmp['resource'] = $resources[] = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
- $tmp['resource']->set_charset('utf8');
- $tmp['resource']->query(sprintf($sql, sprintf('%02d', $i), $uid), MYSQLI_ASYNC);
- $tag = spl_object_hash($tmp['resource']);
- $cmd[$tag] = $tmp;
- }
- }
- $total = $query_times = count($resources);
- //获取结果
- do {
- $read = $error = $reject = $resources;
- //等待查询结束
- if (!\mysqli::poll($read, $error, $reject, 1)) {
- continue;
- }
- //批量获取结果
- foreach($read as $resource) {
- $result = $resource->reap_async_query();
- if ($result) {
- $tag = spl_object_hash($resource);
- $uid = $cmd[$tag]['uid'];
- $month = $cmd[$tag]['month'];
- while(($row = $result->fetch_assoc()) != false) {
- $ret[$uid][$month][$row['access_key']] = $row['sum_amount'];
- }
- $result->free();
- $total--;
- } else die('MySQLi error: '.$resource->error);
- }
- } while ($total > 0);
- 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并发请求,服务端接口示例脚本:
- /**
- * cURL并发请求,服务端接口示例脚本
- * @filename consume.php
- * @url http://test.979137.com/ParallelSQL/consume.php
- */
- $resource = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
- $resource->set_charset('utf8');
- $sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_%d WHERE uid=%d AND product='%s' GROUP BY access_key";
- $sql = sprintf($sql, $_GET['ym'], $_GET['uid'], $_GET['product']);
- $res = $resource->query($sql);
- $out['code'] = $resource->errno;
- $out['message'] = $resource->error;
- $data = array();
- if (is_object($res)) {
- while(($row = $res->fetch_assoc()) != false) {
- $data[$row['access_key']] = $row['sum_amount'];
- }
- }
- $out['data'] = $data;
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode($out);
复制代码
cURL并发请求,客户端调用示例脚本:
- /**
- * cURL并发请求,客户端调用示例脚本
- * @filename p_curl.php
- * @url http://test.979137.com/ParallelSQL/p_curl.php
- */
- //期望结果集:获取以下用户的每个月每个APP的消费统计
- $top = array('979137', '555555', '666666', '888888', '999999');
- $ret = array_fill_keys($top, array());
- $mch = curl_multi_init();
- $opt[CURLOPT_HEADER] = 0;
- $opt[CURLOPT_CONNECTTIMEOUT] = 60;
- $opt[CURLOPT_RETURNTRANSFER] = true;
- $opt[CURLOPT_HTTPHEADER] = array('Host: api.979137.com');
- //生成句柄并加入到批处理
- $cmd = array();
- foreach($top as $uid) {
- for($i = 1; $i <= 12; $i++) {
- $ret[$uid][$i] = $tmp = array();
- $tmp['url'] = sprintf('http://127.0.0.1/ParallelSQL/consume.php?ym=2016%02d&uid=%d&product=SAE', $i, $uid);
- $tmp['uid'] = $uid;
- $tmp['month'] = $i;
- $tmp['resource'] = curl_init($tmp['url']);
- curl_setopt_array($tmp['resource'], $opt);
- curl_multi_add_handle($mch, $tmp['resource']);
- $cmd[] = $tmp;
- }
- }
- //并发执行,直到全部结束。
- do {
- curl_multi_exec($mch, $active);
- } while ($active > 0);
- //获取全部结果
- foreach($cmd as $c) {
- $res = curl_multi_getcontent($c['resource']);
- $http_code = curl_getinfo($c['resource'], CURLINFO_HTTP_CODE);
- if ($res === false || $http_code != 200) {
- die(curl_error($c['resource']));
- }
- $res = json_decode($res, true);
- $res['code'] && die($res['message']);
- $ret[$c['uid']][$c['month']] = $res['data'];
- }
- curl_multi_close($mch);
- var_dump($ret);
复制代码
查询效率对比
1、数据表机构:
- CREATE TABLE `consume_201612` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uid` int(10) unsigned NOT NULL,
- `product` enum('UID','SAE','SC2','SCE','SEM','SCS','SLS') NOT NULL DEFAULT 'SAE',
- `access_key` varchar(255) NOT NULL,
- `service_code` varchar(255) NOT NULL,
- `amount` int(10) unsigned NOT NULL,
- `remark` varchar(255) NOT NULL,
- `data` text NOT NULL,
- `times` int(10) unsigned NOT NULL,
- PRIMARY KEY (`id`),
- KEY `uid_pa` (`uid`,`product`,`access_key`)
- ) 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>
总结
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并发查询的更多相关文章
- mysql 处理查询请求过程
需要搞清楚查询为什么会慢,就要搞清楚mysql处理查询请求的过程: 1.客户端发送SQL请求给服务器 2.服务器检查是否可以在查询缓存中命中该SQL 查询缓存对SQL性能的影响. 1.需要对缓存加 ...
- MySQL慢查询(二) - pt-query-digest详解慢查询日志
一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...
- 深入mysql慢查询设置的详解
set long_query_time=1; #设置慢查询时间为1 秒; set global slow_query_log=on; #开启慢查询日志; show global status like ...
- Linux 实例如何开启 MySQL 慢查询功能
运行 MySQL 时,查询速度比较慢的语句对数据库的影响非常大,这些慢语句大多是写的不够合理或者大数据环境下多表并发查询造成的.MySQL 自带慢查询功能,能记录查询时间超过参数 long_query ...
- mysql慢查询----pt-query-digest详解慢查询日志(linux系统)
一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...
- MySQL简单查询详解-单表查询
MySQL简单查询详解-单表查询 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.查询的执行路径 一条SQL查询语句的执行过程大致如下图所示: 1>.客户端和服务端通过my ...
- mysql慢查询和php-fpm慢日志
MySQL慢查询 在web开发中,我们经常会写出一些SQL语句,一条糟糕的SQL语句可能让你的整个程序都非常慢,超过10秒一般用户就会选择关闭网页,如何优化SQL语句将那些运行时间 比较长的SQL语句 ...
- mysql慢查询开启及分析方法
最近服务维护的公司的DB服务器,总是会出现问题,感觉需要优化一下了,登陆上去,发现慢查询日志都没有开,真是惭愧, 故果断加上慢查询日志, 经过分析sql记录,发现问题很多,开发人员很多没有对sql优化 ...
- MySQL慢查询分析工具pt-query-digest详解
一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...
随机推荐
- 2.用eclipse创建maven Web
一.其他步骤与上一个博客相同,故不赘述,这里要记得选war→Finish 二.在项目上右键选Properties 三.搜索到Project Facets,把勾取消掉,点Apply 四.重新勾选后出现以 ...
- 什么情况下JVM内存中的一个对象会被垃圾回收?
新生代满了会触发 Young GC,老年代满了会触发 Old GC.GC时会回收对象,那么具体是什么样的对象会被垃圾回收器回收呢? 可达性分析算法,判断是否被 GC Roots 引用 判断引用类型:强 ...
- flink 一分钟入门篇
1. 业务说:“…… bulabula……,这个需求很简单,怎么实现我不管?” 面对霸气侧漏的业务需求,由于没有大数据知识储备,咱心里没底,咱也不敢问,咱也不敢说,只能静下来默默储备.默默寻觅解决方案 ...
- 项目组件:分页(pagination)
此分页组件可以辅助完成项目中前端页面分页展示 """ 分页组件应用: 1. 在视图函数中 queryset = models.Issues.objects.filter( ...
- 微信小程序wx.setStorage(OBJECT)
关于微信小程序的:wx.setStorage(OBJECT)在官网API介绍到:
- PHP如何配置session存储在redis
当网站用户量增多的时候,正常的session存取就会出现有点慢的问题,如果提高速度呢. 我们可以使用redis去保存session的会话信息. PHP的会话默认是以文件的形式存在的,可以配置到NoSQ ...
- redis持久化文件问题
问题: Can't open the append-only file Permission denied 发现缺少文件:/data/缺少appendonly.aof,dump.rdb文件. 手动创建 ...
- Linux学习,path,环境变量的配置
方法一: 1.查看当前环境变量配置的所与信息 echo $PATH 注意: echo是输出的意思 加$表示它是一个变量 2.配置环境命令 PATH="$PATH":comdir 注 ...
- ConcurrentHashMap和 CopyOnWriteArrayList提供线程安全性和可伸缩性 以及 同步的集合类 Hashtable 和 Vector Collections.synchronizedMap 和 Collections.synchronizedList 区别缺点
ConcurrentHashMap和 CopyOnWriteArrayList提供线程安全性和可伸缩性 DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含 ...
- 【FreeMarker】【程序开发】数据模型,对象包装
[FreeMarker][程序开发]数据模型,对象包装 分类: Java.FreeMarker2014-10-25 18:49 413人阅读 评论(0) 收藏 举报 FreeMarker 目录(? ...