一次搜索优化之旅
背景
有编辑反映全站检索的速度很慢。检查了一下,发现是由于需要检索的内容存在于两个表的title和content字段,两个表有共同的主键ID,查询的时候是left join两个表,再使用like语句去匹配内容。一旦数据比较多就会耗时严重。 直接把sql语句复制出来,在navicat中跑一下,两个表都是2w多的记录,跑下来大概需要10多秒的时间。
第一次优化
从切分出发,如果一次全表left join导致数据太多,那么就切分,使用3000作为步长,每次最多只查询3000条。
- 获取本次查询最小和最大ID,根据时间判断
- 根据最小最大ID,按步长切分成多个URL
- 前端点击“搜索”按钮,同时请求这些URL,然后使用promise.all,在所有请求完毕之后,将结果存在一个数组里面
- 前端根据ID进行排序,做好分页显示
这样部署了之后发现,根本没效果,请求还是一个一个的发送,完全没有显示并发的功效。 一番查找,原因是:
开启session_start以后,这个session会一直开起,并且被一个用户使用。其他用户开启session的话要等待第一个session用户关闭以后才可以开启session,这样就造成了session阻塞。由于PHP的Session信息是写入文件的,1个客户端占有1个session文件。因此,当 session_start被调用的时候,该文件是被锁住的,而且是以读写模式锁住的(因为程序中可能要修改session的值),这样,第2次调用 session_start的时候就被阻塞了。
解决办法很简单,加上:
session_write_close()
在session_start之后调用
效果很明显,之前十多秒的搜索,目前只需要2-3秒就可以了。一切很完美,收工。 但是,换了一个数据量更多的站点就傻眼了,这个站点的表有40多万数据,请求时间居然需要50多秒了。这是为啥呢?
第二次优化
经过一番查找,原因出在浏览器端:
浏览器对请求数有限制。主流浏览器对同一域名的并发请求数限制大部分都是6。
也就是说,针对40万的数据,我切分了140个请求,虽然我同时将这些请求发出去了,但是浏览器却还是每6个请求发送一次,等这批完成了,再发送下一批请求。这样就导致要20多次才能完成发送,每次的请求需要完成需要近10秒,总共需要50多秒才能搜索完成。这种比之前连表查询速度还慢。
我们没办法去修改浏览器的限制,那问题还是回道从前了,需要减少发送请求的次数,但是还是要切分请求,那出路还得是在后端。将并发的工作放到后端,因为后端是没有请求数限制的,修改之前的步骤:
- 获取本次查询最小和最大ID,根据时间判断
- 发送请求至proxy请求
- proxy请求根据ID,切分成多个URL
- 使用curl_multi_init、curl_multi_exec等并发请求,并将返回数据进行合并
- 前端根据返回数据分页显示出来
这样下来既可以避免前端并发限制,也可以提高数据库使用效率。 经过测试,之前50多秒的请求,目前稳定在10秒左右,如果不是全表like,指定时间段就会更快。
代码
前端的promise.all,虽然最终没有用上,但是还是比较优雅的
async function fetchAll(urls) {
const fetchPromises = urls.map(url => fetch(url));
const responses = await Promise.all(fetchPromises);
const dataPromises = responses.map(response => response.json());
return Promise.all(dataPromises);
}
fetchAll(urls).then(data => {
let datas = [];
if(data){
//合并数组
for(let i = 0; i < data.length; i++){
for(let j = 0; j < data[i].length; j++){
datas.push(data[i][j]);
}
}
//排序
if(datas.length){
datas.sort(function(x, y){
return y.id - x.id;
});
searchResult = datas;
const totalCounts = datas.length;
const pageSize = 25;
$('#pages').hide();
//前端分页,组件使用的是 https://jqpaginator.keenwon.com/
$('#pages2').jqPaginator({
totalCounts: totalCounts,
pageSize: pageSize,
visiblePages: 10,
currentPage: 1,
first: '<li class="first"><a href="javascript:void(0);">首页</a></li>',
prev: '<li class="prev"><a href="javascript:void(0);">上一页</a></li>',
next: '<li class="next"><a href="javascript:void(0);">下一页</a></li>',
last: '<li class="last"><a href="javascript:void(0);">末页</a></li>',
page: '<li class="page"><a href="javascript:void(0);">{{page}} / {{totalPages}}</a></li>',
onPageChange: function (num, type) {
buildResult(num);
$('#pages2').append('<li class="page"><a href="javascript:void(0);">共'+searchResult.length+'条</a></li>');
}
});
$('#submit').removeAttr('disabled');
}else{
$('#pages').hide();
$('#searchResult').html('');
$('#submit').removeAttr('disabled');
}
}).catch(error => {
console.error('Error fetching data:', error);
});
后端并发请求
$mh = curl_multi_init();
$curlArray = [];
foreach ($urls as $i => $url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch);
$curlArray[$i] = $ch;
}
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
$responses = [];
foreach ($curlArray as $i => $ch) {
$responses[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
$data = array();
foreach ($responses as $i => $response) {
if($response != null){
$r = json_decode($response, true);
if(count($r)){
for($i = 0; $i < count($r); $i++){
$data[] = $r[$i];
}
}
}
}