实战优化php和mysql:记录一次实际项目中优化数据库访问

实战优化php和mysql:记录一次实际项目中优化数据库访问

本期中某个项目中下载报名数据模块中,项目经理说正式环境下出现下载过慢的情况,在实际的数据量,一个文件需要半分钟甚至一分钟的时间生成,从而导致超时,甚至下载失败的情况.因为这个模块是之前已经离职的同事写的,因此我去接手还是有点茫然的.最后在我的优化下将下载时间优化到10s以内.还是要有点小收获.因此写下这一篇文章.


同步环境

首先是把线上的数据库同步下来,这样才能模拟出来,否则在测试环境下不过是数据链较小的测试数据,不还查看.同步之后发现数据量并不大,单张表,最大的不过8M,五万行.就算是下载报名的模块涉及到的表也不过五六张,加起来十来m的数据,按理说不应该出现访问过慢的情况.

降低代码粒度

先看代码吧,好家伙一个目录下十几个文件夹.只能先去下载在看到底是那个文件.打开文件后发现几个下载模块都混在一起,单个文件下大约在1000行左右,而且不是函数,而是一个switch case区分下载模块.如此一来优化难度就大幅度提高了.

在这里必须强调一个原则,那就是代码的小颗粒度原则.

  • 同一层级下大小不宜过大.同一个层级下如果内容过多会导致阅读障碍,也不利于优化.比如同一个目录下十几个文件的话就要划分子文件夹.
  • 不要吝惜使用小函数.函数是ide友好的.尤其是添加过注释的函数.大的模块,划分为几个小的函数是有利于调试与阅读的.虽然有可能带来性能的损耗,但是更有利于编写与优化.
  • 如果某个模块,你觉得有必要添加注释,那么请添加函数.

根据上面的原则,我首先把下载模块独立出来.有把几个下载模块独立成同一个目录下的几个文件.这样单一文件下就不超过200行.也就是一个屏幕可以看完的大小.

在优化的过程中通常有一个大的原则.在不熟悉代码的前提下,尽量不要修改代码的逻辑.在这里我只是对于代码进行了拆分,并没有修改代码.在测试环境测试一下,还是很慢,2分钟左右.

环境优化

和线上环境比测试环境环境要快一点.ok,环境不一致这是肯定的,去线上环境看了一下,发现原来正式环境是很早配置的.并没有运维优化过,于是在晚上,我把数据库配置文件进行了修改,扩大了内存,线程数,php环境有按照自己之前写好的文件配置了一下.测试一下,快了大概30s.关于如何优化数据库配置文件,我这里推荐高性能mysql
这本书的里面有一个章节讲这个内容.

聚焦特定模块

深入理解计算机系统有一个推论,那就是绝大多数情况下,代码的时间都花在某个循环模块中.在这个结论下我们只要找到这个模块,优化他就能大幅度的降低时间复杂度.

如何找到这个模块,phper我有几个建议

  • 理解整个流程,通过数据量比对,大体能够猜测出代码中那个模块耗时多
  • 黑盒测试配合代码注释.通过注释掉某个模块,在测试可以找到那个模块耗时大
  • 探针.php和操作系统都提供了 DTrace 探针,来追踪函数的调用次数,消耗时间,系统情况.但是有一个大前提,你的代码大量使用函数,而不是大颗粒度下堆在一起.

当然这里我并不会使用探针(其实是我用的少).

通过黑盒测试的使用我把主要耗时的代码模块找到了

$gamename = $db->get('game', 'name', array("id"=> $_GET['gid']));

$row = $db->select('game_team', '*', $where);
foreach($row as $k => $v){
  ...
  ...
  $teamappl = $db->select('game_team_apply', '*', array('AND' => array("game_team_id"=>$v['id'],"type"=>"1")));  //赛事报名  
  foreach($teamappl as $k1 => $vb){
    ....获取学生信息
    //获取学生报名组别
    //获取项目id 已经参加的项目 就爆出来哪个项目
    //为关联数组 $k 从1开始
    $item_short_name = '';//表内容中 项目的项目名称

    foreach($item_id_array as $k3 => $v3){
        $ifitemname = $db->get('game_item_eroll', '*', array('AND' => array('gameitem_id' => $v3,'apply_id' => $vb['id'])));
        if(!empty($ifitemname)){
            $item_short_name = $db->get('gameitem', 'short_name', array('id' => $v3));
            $tmp['item'.$k3]= $item_short_name;
        }else{
            $tmp['item'.$k3] = '';
        }
        $game_type = $db->get('gameitem', 'type',array('AND' => array('id' => $v3)));//获取赛事信息
        if(!empty($game_type) && $game_type == 2){
                $duiwu_groupname = $db->get('game_item_eroll', 'groupname', array('AND' => array('gameitem_id' => $v3,'apply_id' => $vb['id'])));//获取赛事报名记录
                $tmp['group'.$k3]= !empty($duiwu_groupname)?$duiwu_groupname:'';
        }

    }
  }

解释一下上面的代码,这是一个获取学生报名信息的下载模块,首先向循环报名的赛事,然后对每个赛事底下报名记录进行循环.在每一个记录下再循环获取学生的报名信息,再对查找每一个学生报名过的赛事.

这样看下来总算找到原因了.原来是在多重循环里面多次访问数据库如果有很多学生的话,就会同等数量级的数据库访问.即便mysql是轻量级的数据库,如此庞大的访问依旧扛不住.在这里我给出一个普遍的数据库访问经验.

  • 避免在循环内访问数据库.如果必须的话,请限制循环的次数

添加索引

首先想到的办法是优化索引.看了一下数据表,有索引但是没有优化过(其实这一步应该放到最后一步的).关于索引的建立与使用,我之前写过一篇文章可以进行参考.通过对几个表添加索引,将访问时间提高到了30s左右.

减少数据库访问次数

通过查看代码我们发现在最底层的循环内,两次访问了同一张表.ok,这四次访问可以进行合并

foreach($item_id_array as $k3 => $v3){

    $erollInfo = $db->get('game_item_eroll', ['groupname', 'id'], array('AND' => array('gameitem_id' => $v3,'apply_id' => $vb['id'])));
    $ifitemname = $erollInfo['id'];
    $gameItemInfo = $db->get('gameitem', ['type', 'short_name'],array('AND' => array('id' => $v3)));
    if(!empty($ifitemname)){

        $tmp['item'.$k3]= $gameItemInfo['short_name'];
    }else{
        $tmp['item'.$k3] = '';
    }
    $game_type = $gameItemInfo['type'];
    if(!empty($game_type) && $game_type == 2){

           $duiwu_groupname = $erollInfo['groupname'];
            $tmp['group'.$k3]= !empty($duiwu_groupname)?$duiwu_groupname:'';
    }
}

我们再看这一行

$gameItemInfo = $db->get('gameitem', ['type', 'short_name'],array('AND' => array('id' => $v3)));

获取每一个赛事的信息.在这里我们可以把他放到循环的外部,

$gameItemInfo = $db->get('gameitem', ['type', 'short_name'],array('AND' => array('id' => $item_id_array)));

然后对他进行重新排序,在循环内,只访问数组.同理对于另个访问也可以做一样的操作,这样就把内部循环的减少数据库访问次数.把数据库的放到了数据库服务器上,也便于优化数据库.

通过上面一系列的操作,就把降低了数据库的访问.在重新测试一下,访问速度提高到了10s以内.ok很不错了,再把其他模块优化一下,吧不必要得到数据库访问和循环提出来.很好已经到了可以接受的范围内了

缓存表

通过上面的一系列优化,我们已经把代码优化到了我们容忍的范围内.如果还要继续优化,却有一个限制,那就是最后一个循环并不是纵向循环,而是对于横向的学生进行访问.这样就不能把他合并到前面的循环内.如何继续优化.这时候我的领导提出了一个办法,添加一个缓存表,定时吧学生的报名数据汇总到一个缓存表中,与学生关联起来,这样在下载的时候就不需要重温遍历数据库,而只是需要访问缓存表

这样的办法,在高性能mysql中也有提及,那就是缓存表(汇总表)与计数器表,通过提高接口的时间来加快数据统计的效率.当然这里我并没有进行这一步操作.最要是是没有时间(笑),如果之后这个项目数据量进一步扩大,那么确实有添加汇总表的需求了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章