原文是在我本身博客中,小伙伴也能够点阅读原文进行跳转查看,还有好听的背景音乐噢背景音乐已取消~ 2333333
大爷我就算功能重作,模块重构,我也不作优化!!!运行真快!php
本文主要探讨的核心是【为何不要在循环中使用数据库操做?】
用了一个例子来讲明为何不要这样作的缘由以及当遵循了这条规则后,所带来的好处:代码运行效率的提高、心情好(乱入-_-)之类的。sql
最近在对一个老项目进行维护的时候,发现有一个页面加载很耗时,响应速度在1.7s以上,并且这个页面粗略看起来须要加载的东西也不是不少,为何加载会这么慢呢?本着一探究竟和对这些慢响应没法忍受的态度去看了一下,发现它的代码写的很糟糕,处处都是循环,并且还在循环中进行了sql查询。后来在本身的优化下,从均加载1.5s到均0.02s,实现了一个质的飞跃。
本文,就是总结一下,本身在遇到这种代码的处理方式,以及思想的演化数据库
本文所要优化的是一段,由权限控制的菜单,共有两级。并且须要在特定的菜单位置上显示待办事项的数量。普普统统的一段权限控制菜单访问的功能,其实处理起来也就是多了一个【特定菜单位置上显示代办数量】的功能,简单思考一下,只要找到对应的菜单id,在其上面增长一个对应的数字就能够了。想是这么想,作起来呢?数组
遇到网页加载很慢的时候,首先要肯定究竟是哪一部分加载很慢。能够经过浏览器f12打开调试工具,在network选项里,查看当前页面上每条资源的加载耗时状况来推断。以个人博客某篇文章加载为例:浏览器
最右边有个红框标识的就是每条资源的加载耗时,咱们能够看到第一条是php服务端的处理速度。下面的即是各类资源了。我要优化的那段业务中,发现正是由php服务端处理加载过慢带来的巨大耗时,平均每次这里加载须要1.5s以上。其余资源的加载速度平均都是在几十ms,那么就能够肯定是这段php写的有问题了。缓存
接下来咱们就能够直接去看php代码了。socket
找到对应的代码块,测试了一下这段代码块的处理时间,发现用时1.5s之多,有点震惊。简单看了一下代码,两大段过百行的代码块,通过一段时间的分析,发现有不少重复的、没必要要的地方,现整理代码逻辑(伪代码)以下:函数
<?php /** * 一、取出一级菜单 并循环一级菜单 */ foreach ($top_menu as $top_id=> $value1) { /** * 二、取出二级菜单 并循环二级菜单 */ foreach ($second_menu as $key2 => $value2) { /** * 三、取出三级菜单 循环三级菜单 当前菜单项含有url信息 * 四、对权限进行验证 判断当前主菜单下是否拥有能够访问的权限 * 五、对顶级菜单须要显示的待办事项作处理 */ foreach ($third_menu as $key3 => $value3) { // 权限验证 $flag = $this->auth->check($ctrl, $action); /** * 作处理 在顶级菜单上增长待办事项数 * to do something */ // ............ // ............ /** * 这里奇葩的是又调用了另一个方法 * 传递了一个top_id 一级菜单ID * 而后根据一级菜单重复二、3在对应的三级菜单上再增长待办事项 */ $this->handle_son_backlog($top_id, $backlog_data); } } }
这段代码块都作了什么呢?文字简述以下:工具
ctrl
(控制器)和action
(方法)好了,以上就是第一个函数的做用,然而,这还没完,在循环三级菜单的时候,又调用了另一个方法handle_son_backlog()
,这个方法传了两个参数,一个是一级菜单id,另一个是待办事项数组,那么这个方法又作了什么呢?性能
理解完原来代码的用意后,再修改起来就不难。原本打算再本来的基础上修改,可是用了一段时间发现,代码写得太乱,根本没办法在看,因而我决定,本身写,先改造一部分,去掉多余的第二个函数
改变代码块的可读性;
通过第一次想法的修改以后,去掉了第二个方法多余的循环、重复验证的问题,代码变得稍微精简一些了:
/** * 对特定的菜单进行处理 增长待办事项 * @param array &$son_data 子菜单信息 * @param array $backlog_data 待办事项数据 * @return array */ function handle_son_backlog(array &$son_data, array $backlog_data) { if (empty($son_data['id'])) { return false; } switch ($son_data['id']) { case '': $son_data['backlog_num'] = (isset($backlog_data['xxx']) && empty($backlog_data['xxx'])) ? $backlog_data['xxx']: ''; break; default: # code... break; } return $son_data; } /** * 获取菜单 * @param array $backlog_data 待办事项数据 * @return array */ function get_menu() { /** * 一、取出一级菜单 并循环一级菜单 */ foreach ($top_menu as $key1 => $value1) { /** * 二、取出二级菜单 并循环二级菜单 */ foreach ($second_menu as $key2 => $value2) { /** * 三、取出三级菜单 循环三级菜单 当前菜单项含有url信息 * 四、对权限进行验证 判断当前主菜单下是否拥有能够访问的权限 * 五、对顶级菜单须要显示的待办事项作处理 */ foreach ($third_menu as $key3 => $value3) { // 权限验证 $flag = $this->auth->check($ctrl, $action); /** * 作处理 在顶级菜单上增长待办事项数 * to do something */ /** * 对子菜单的待办事项作处理 */ $this->handle_son_backlog($value3, $backlog_data); } } } }
修改好以后,运行0.6s,快了一倍,可是这确定是不够的。仍是慢!!!
使用递归结构;
略看第一次修改后的代码仍是有能够提速的地方。三层循环写的着实让人辣眼睛啊,由于在循环中还有数据库操做,请注意:任何在循环中参与数据库的处理都是不明智的选择。在大脑中构思了一下,其实这些彻底能够经过递归来实现嘛。只须要把菜单一股脑取出来,在用递归造成树形结构就能够了。说干就干
先说说我这段处理大体思路:
递归的方法中,作一些特殊处理
差很少就是如上几步思路,完成版伪代码以下:
/** * 对菜单进行递归处理 并验证权限 增长待办事项数量 * @param array &$menu 菜单 * @param array $backlog_data 待办事项数据 * @param array $menu_list 原来的菜单 * @param int $pid pid * @param int|integer $last_pid 父菜单id * @param int|integer $i 递归标识(用于执行特定操做) */ function get_handle(array &$menu, array $backlog_data, array $menu_list, int $pid, int $last_pid = 0, int $i = 0) { foreach ($menu_list as $key => $value) { if ($value['pid'] == $pid) { if ($i == 1) { // 要验证的url $check_url = explode('?', $value['url']); // 拆分红uri数据段 $check_url_arr = explode('/', $check_url[0]); // 控制器名 $ctrl = $check_url_arr[0] . '_' . $check_url_arr[1]; // 方法名 $action = isset($check_url_arr[2]) ? $check_url_arr[2] : 'index'; if ($this->auth->check($ctrl, $action)) { $menu[$last_pid]['zi'][$value['type_id']] = $this->handle_son_backlog($value, $backlog_data); } } else { $this->get_handle($menu, $rule_list, $backlog_data, $menu_list, $value['type_id'], $pid, 1); } } } } /** * 获取菜单 * @param array $backlog_data 待办事项数据 * @return array */ function get_menu(array $backlog_data) { // 获取菜单列表 $menuList = $menuModel->get_list(['id', 'name', 'pid', 'url'], ['version' => 1]); // 取得一级菜单 foreach ($menuList as $key => $info) { if ($info['pid'] == 0) { $menu[$info['id']] = $info; } } foreach ($menu as $id => $info) { // 对菜单做递归处理 $this->get_handle($menu, $backlog_data, $menuList, $info['id']); /** * 判断当前主菜单下是否有子菜单 若是没有则释放掉当前一级菜单 * 若是有则对当前一级菜单进行待办事项处理 */ // // // } return $menu; }
差很少了就来进行调试一下吧,运行一看0.3s,感受跟第一次修改的时候运行的也差很少嘛!(这时候已经比最初的运行速度提高了差很少4倍。)但隐隐以为这还不够...
减小数据库查询次数;
从新梳理一下代码逻辑,试图找到能够优化的点。在梳理的时候注意到一个地方,就是$this->auth->check()
这个检查权限的方法了。去跳转查看了一下,发现这方法也是查一次查一下数据库,这样的话,综合起来,这里仍是牵涉到在循环中查询数据库的操做了。这块必须优化。
若是把当前登录者已拥有的所有权限都取出来,替换掉check()这一块,是否是效率就会更快些?感受答案应该是确定的!
在通过一些调整以后,发现程序执行的速度有了极大的提高,增长了一段取出全部权限的操做:
/** * 获取用户全部权限列表 * @param int $user_id 用户id * @return array/boolean */ function get_user_operation_list(int $user_id) { $group_ids = $this->get_value_by_pk($user_id, 'groupid'); if ($group_ids) { $group_ids_arr = explode(',', $group_ids); // 取出用户所拥有的权限 控制器和方法名 $result = $this->db->select('o.module, o.action') ->from('admin_group_operations ago') ->join('operations o', 'ago.operations_id = o.operation_id', 'left') ->where_in('ago.group_id', $group_ids_arr) ->where('o.operation_id >', 0) ->get() ->result_array(); if (!empty($result)) { $new_data = []; // 生成指定的键值对 foreach ($result as $key => $value) { $new_data[] = $value['module'] . '/' . $value['action']; } return $new_data; } } return false; }
而且在$this->auth->check()
这行替换成了in_array($ctrl . '/' . $action, $operation_list
。这样就差很少了。
运行一看,速度也挺喜人。居然达到了0.014,比最原始的快了百倍不止。
而后再去看网页运行,发现我优化的这块,明显比网页上的其余模块加载速度要快了许多(由于项目用了iframe),以前是其余模块的内容出来了,头部的菜单还没出来。如今的状况偏偏相反,头部菜单最早加载出来,而后等待其余iframe的加载。
作完这番工做,长舒一口气,这一番coding没有白费。
从这个例子中,咱们能够获得一些,代码优化的技巧:
好像就只有这个吧....2333333
能不可以继续优化呢?放在缓存中会如何?
若是放在缓存中的话,也不是不行,可是这里有一个点就是这里的待办事项是可变的。并且项目中也没有使用socket的技术。若是单单存储在缓存中的话,那么更新缓存里的这块数据就会变得更加啰嗦。索性就暂时这样放着,能之后性能指标提升了,再来优化。
结。