原文是在我本身博客中,小夥伴也能夠點閱讀原文進行跳轉查看,還有好聽的背景音樂噢背景音樂已取消~ 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的技術。若是單單存儲在緩存中的話,那麼更新緩存裏的這塊數據就會變得更加囉嗦。索性就暫時這樣放着,能之後性能指標提升了,再來優化。
結。