背景:由於以前的應用多且雜,所以最近對公司的應用進行優化改造,需要所有介面RT達到xxx值以下。
一、監控
那麼問題來了~現在應用都是放養式的,幾乎沒有什麼監控工具,不可能根據log一個介面一個介面去撈日誌,那怎麼知道哪些介面rt長,需要優化呢。 所以第一步我們做的事情就是上監控。
監控工具:pinpoint。
選擇pinpoint有幾個方面的考量:
1.對應用程式碼0侵入,這個當然是我們程式設計師最關心的,誰都不喜歡因為附加功能在自己的應用大動干戈,萬一影響原有業務就不值得了。。
2.應用依賴關係,呼叫關係一目瞭然,通過一個traceId將所有流程串起來,方便排查問題。這一點也是非常重要,在分散式系統中,呼叫關係請求太複雜,如果沒有traceId標識,你是很難找到這個請求的上層呼叫關係的。
3.介面各種監控圖表,當然也包括RT圖表,便於快速定位需要優化的鏈路。
pinpoint的原始碼及教程可參考https://github.com/naver/pinpoint。
我們可以大致看一下pinpoint的監控頁面,如下圖
從圖中我們能得出:呼叫鏈路一列看出該請求實際執行了2次SQL。
執行時間一列看出第一次耗時19ms,第二次722ms。
traceId是此次請求的唯一標識,如果我們在應用需要記錄該值,也可通過對應API獲取。
當然這個示例只是一次很簡單的請求,他還能監控dubbo呼叫、http請求、Redis、db等等操作,所以只要應用配置了它,那麼該請求執行的呼叫細節就一目瞭然,盡在我們掌握之中,而不是放養任其發展。
二、優化
既然目標已經被我們揪出來了,那我們下一步當然就是把它解決掉,優化它。我從優化過程中得出一些很普遍,實用的優化經驗,特別是對於初學者,可以多多參考。因為對於剛開始程式設計的來說這些就是習慣,正常的邏輯,但並不代表它就是最優的。
1.迴圈SQL操作
這類情況對於新手來說很容易犯,比如就建立交易訂單。我們主子單關係如下:
那麼建立訂單需要生成1個主單,N個子單,這時很多這麼寫(程式碼僅表示執行流程):
insert(主單);
for(子單:子單列表){
insert(子單);
}
複製程式碼
所以就出現迴圈SQL操作,因為SQL操作比較耗時,迴圈的話就會大大拉大整個介面的rt時長,這種情況我們應該一次性把所有子單insert,而不是一次一次地操作,優化後程式碼類似為這樣:
insert(主單);
batchInsert(子單列表);
複製程式碼
Mybatits是支援迴圈標籤的,所以在sqlMap檔案裡改造一下SQL就可以了。另外批量update也是可以的,執行批量操作需要在資料庫連結加上引數allowMultiQueries=true
2.資料庫索引
索引當然是必須要建的,不然得查到什麼時候。不過建歸建,但是我們還要正確的運用它。
我們可以通過explain命令檢測SQL是否使用索引。
EXPLAIN SELECT * FROM article WHERE id=10;
key和rows反應了使用的索引以及預計需要掃描的行數。 還有一些我們需要注意的:
在我們優化Query語句中的ORDER BY的時候,儘可能利用已有的索引來避免實際的排序計算,可以很大幅度的提升 ORDER BY操作的效能。在有些 Query 的優化過程中,即使為了避免實際的排序操作而調整索引欄位的順序,甚至是增加索引欄位也是值得的 因為MySQL中,order by的實現有兩種型別:
1).一種是通過有序索引而直接取得有序的資料,這樣不用進行任何排序操作即可得到滿足客戶端 要求的有序資料返回給客戶端;
2).一種則需要通過 MySQL 的排序演算法將儲存引擎中返回的資料進行排序然後再將排序後的數 據返回給客戶端
我們建的索引為user_id
,define_id
,seq
聯合索引,可以看出第一張圖排序define_id直接走索引,第二張圖走不了索引需要額外的排序操作Using filesort.group by 其實也是進行了order by操作 然後進行分組,所以group by也是類似的優化方式。
DISTINCT儘量少用,DISTINCT其實也是進行了一次group by操作,然後每一組取的第一條記錄。
java應用的型別一定和資料庫索引列的型別匹配,例如java型別為long,資料庫型別為varchar,這樣去查詢是用不了索引的,但是不會報錯。 下圖是兩種方式的對比:
3.count計數千萬不能用
計數也是我們經常用到的,比如優惠券領取數量,統計活動參加人數。這些有時是和業務強耦合的。比如秒殺只能賣出多少件等等。這類場景我們千萬不能使用count來計數。我們來分析一下為什麼不使用count:
1).count會查詢所有滿足條件的記錄,如果表非常大,這將可能導致全表掃描,這後果大家都知道的吧。 2).如果使用count來控制,那麼在業務邏輯執行期間,肯定要加鎖,否則剛才count的結果就白操作了,這將也會阻止所有對該活動的請求。
這種場景我們一般都是在需要控制的記錄上加個計數字段,比如控制最大領取值num=10個。那在業務邏輯裡面可將對總數控制轉為通過SQL的方式,
update xxx set num=num-1 where id=xx and num>0;
這是原子操作,可保證一定不會超領。根據返回結果判斷是否執行成功(返回結果為影響的行數)。
4.鎖
單機鎖和分散式鎖,鎖其實我們能夠避免就不要使用它,因為加鎖就代表只能序列執行,併發數降為1,這肯定影響效能。
如果是類似庫存扣減的場景,可參考第3條。通過資料庫的原子操作來避免。
如果是更新等操作,可通過樂觀鎖來避免長時間的阻塞。
如果非要使用鎖,不管是單機鎖還是分散式鎖,我們一定要評估該鎖的影響範圍,是針對單個使用者userId還是所有的使用者userId,單個使用者是可以採用的,因為單個使用者併發度極低,但是如果是所有使用者的操作加鎖,那一定要好好評估,這個操作會導致所有使用者的類似操作阻塞。
5.適當冗餘
在分散式系統中,冗餘應該是非常常見的情況。我們這個時候就不要追求資料庫正規化的標準了,因為按照資料庫第幾正規化來設計,所有欄位都不冗餘,但是這給我們查詢帶來很大麻煩。可能我們查一個訂單,需要關聯查詢,查子單資訊,查商品資訊,查支付資訊等等,查詢這麼多,想想我們介面的rt能快嗎,qps能高嗎。將商品名稱等基本資訊冗餘可減少對其實模組的查詢,未嘗不是一種好的方式。
6.Redis快取
其實快取在我們系統普遍都用到了,所以對這部分優化不多。還是總結一些經驗。 快取的重新整理策略選擇:失效重新整理還是定時重新整理。 因為監控到很多介面RT總是有規律的變慢,這是因為都是在快取失效的時候,需要從db及其他模組組裝資料,然後推到快取,這時所有請求都走不了快取,在流量大的時候也有可能成為致命的因素。如果是這種情況,例如首頁推薦商品、推薦帖子等等訪問量大且相同的場景可以通過定時重新整理的方式。 Keys*命令線上嚴禁使用:Redis是單執行緒,該命令的執行將會導致所有後續請求阻塞,影響整個系統效能。
7.搜尋引擎
可能看到這個比較疑惑,搜尋引擎怎麼還能優化RT,他比DB當然慢,但是在某些場景他可以比DB更快。例如一個社群論壇,需要對文章進行篩選排序,總共十來個欄位,而且自由組合,這時DB就無能為力,因為條件太多,各種排序操作,關聯操作,資料庫沒辦法建索引。我們可以通過將A、B表中的欄位構建成完整的資訊,推送到搜尋引擎,查詢的時候直接根據條件搜尋。這樣既能保證rt,又能避免DB被複雜查詢拖垮。