Java接口全鏈路優化:如何降低接口RT時長(轉)


背景:由於以前的應用多且雜,所以最近對公司的應用進行優化改造,需要所有接口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被復雜查詢拖垮。


作者:年糕媽媽技術團隊
鏈接:https://juejin.im/post/5bf23d12e51d4514e05123cf
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM