天天看點

查詢接口性能優化實錄,講點新手也能用的

作者:Java解白

前言

注意看,這個男人叫小帥,他正在做一個老系統的重構,其中有一個查詢接口慢到了不得不改的地步,面對完全不熟練的項目,一臉懵逼的他會采用什麼樣的手段來脫離困境呢,讓我們一起走進科學。嘴瓢了,哈哈,天降大任于斯人也,必先送其屎山,請叫我掏糞男孩。騷話說過了,接下來說點正經的。其實我個人還是比較喜歡這樣的優化任務的,平時我也會通過我的代碼風格以及言傳身教(誇張一下,都别打我)來影響同僚們,我感覺大家的技巧是有提升的。但是呢,一直以來也沒有一個具體的總結,那麼今天就是聖劍解封之日,新手接口優化完全指南,大家一起來裝逼吧!

接口背景

查詢接口性能優化實錄,講點新手也能用的

查詢頁面如圖,總數其實不多就1W3,清單橫向展示約90個字段。關于這裡展示字段過多的問題,我提一嘴,這是使用者強制要求的,并且該系統已上線,是以這個問題不能叫做問題。除此之外,該清單也并非普通的單表查詢,存在聯查和字段填充,約30個字段存在額外邏輯,其中最要命的庫存字段需要實時查庫,也就是說不能緩存。本項目為部門核心項目,是以查詢細節不好說,大緻就是會經過很多運算,最後拼成一個清單傳回給前端。沒錯,大家熟悉的屎山就是這樣,又臭又長。

接口現狀

我,作為京城牢頭兼優化小能手,自然是當仁不讓(嗚嗚嗚),成了被選中的人。項目作為核心項目,自然是有着核心的複雜度,至少剛看到項目代碼和聽人講解了一遍後,我感覺和沒聽一樣,淦。給了一天的時間來優化,這個感覺,對味了,仿佛回到了之前做看闆項目的時候(詳見設計方案-大資料量查詢接口優化)。接口的現狀自不必多說,代碼中少的可憐的注釋,僅有的注釋也看不明白(剛開始不懂業務),大概五百行的核心代碼和一堆到處亂跳的内部方法以及好幾坨大SQL......令人絕望的現狀,也沒辦法,拼好破碎的道心開始着手進行優化。

喔,對了,說一下之前的狀況。該查詢接口畢竟是高頻查詢接口,而且已經上線了,之前是已經優化過一輪了,據說沒優化前甚至會觸發網關逾時。至于大半年後為啥又出現了接口慢的問題,應該是跟聯表和多個資料源的資料膨脹有關,導緻當時快的子查詢慢慢變得性能低下了。

優化及梳理準備

一個查詢接口的優化,我的思路還是比較簡單,先上通用解決方案,再根據業務邏輯進行深度定制優化。熟練的打開查詢頁面,按下F12鍵,拿到查詢接口入參和對應接口位址。拉下代碼,問問同僚主分支用的哪個,将正式資料同步到開發環境,啟動項目,postman調用接口,一氣呵成。

查詢接口性能優化實錄,講點新手也能用的

我測,15s,好家夥,怪不得要優化,我要是使用者也受不了。并且前端展示字段是配置化的,也就是說清單查詢接口還有前置的頁面配置擷取,加上前端渲染後,其實體感更慢。切到IDEA,看看代碼,捋了捋,差點背過去,五百行的代碼加上七八個SQL,還缺少注釋......我先暈一會兒。

活了,接着奏樂接着舞。一般我将這種複雜查詢類的接口,内部邏輯代碼按照類型分為三種,資料源、計算、拼接。

  • 資料源型,一般是通過SQL或者外部接口擷取資料。
  • 計算型,一般是某些字段需要經過計算得到,這部分代碼也要單獨标記,優化的時候拆分出來
  • 拼接型,通常查詢沒有這一步,但是部分動态表格,前端會要求特殊的資料結構,這種情況下會有,但是一般也不需要優化。

如果不想重構代碼的話,可以簡單寫個TODO打個醒目的标記,特别是資料源和計算類的代碼,這将會是我們進行優化的重點。在梳理期間還可以做一下小優化,這些優化因為不太影響性能,是以不作為優化手段展示。

梳理代碼

當然在進行這一步的時候,我會選擇按照IDEA的醒目提示以及對代碼的了解幹掉一些無用和重複的代碼。同時盡量重新定義變量名,改成自己能看懂的,而不是abc123之類的。

查詢接口性能優化實錄,講點新手也能用的

代碼剪枝

代碼剪枝的意思是如果内層函數沒有别的引用,我會選擇将代碼合并到目前方法中,避免亂跳,看着麻煩。

合并循環

像這種不影響其他部分計算的循環,完全可以合并到其他必要的循環中,減少循環。這裡隻有一頁查10條資料,循環10次比較少,看不出優化效果,但是這是個好習慣,不是嗎?

查詢接口性能優化實錄,講點新手也能用的

抽取公共變量,避免重複操作

這一步就是梳理資料源的意義所在,當代碼行數過多,并且有幾個開發接手過的情況下難免發生。一個查詢A,我在方法A中寫過了,但是到了方法B,可能是新加的功能,換人了或者忘了之前寫過,又重複一次查詢A,這完全是沒有意義的。

查詢接口性能優化實錄,講點新手也能用的

優化手段

所有的優化并不是無芯浮萍,針對問題去優化才是我們該做的,不要在收益不高的地方去使勁。在做完梳理準備後,将懷疑代碼塊圈起來列印運作時間,這種比較樸素,沒辦法再這樣。

查詢接口性能優化實錄,講點新手也能用的

我是推薦大家搭配性能分析插件來定位,比如XRebel、Skywalking。記憶體和CPU一般不在查詢接口優化的考慮範圍,當然要看的話,我推薦IDEA自帶的Profiler插件。在本次優化中,我是使用了IDEA插件市場裡的XRebel來進行接口性能分析,具體的安裝和使用可以網上搜一下,分析結果如下圖。

查詢接口性能優化實錄,講點新手也能用的

這裡是19秒,正常,因為正式伺服器和資料庫比我本地性能要高,自然要快一些。這都不重要,細看一下圖,很清晰,甚至連可執行的SQL都放上去了。每一行從左到右依次是執行時間、方法名,詳情裡面是具體的可執行SQL。通過觀察可以發現耗時比較長的有setDeliveryCalculationInfo中的setStock和setCalculationInfo方法,具體是因為SQL查詢慢了,定位到了問題代碼塊那就可以着手進行優化了。以下僅通過實戰的角度介紹作用夠大的方法,就不多講廢話了。

緩存

緩存真的是空間換時間的首選利器,分布式緩存Redis或者本地緩存Caffeine任君選擇。雖然是額外增加了中間件,但是現在項目一般都會有吧,是以在我看來這是改動收益成本效益最高的一種方式。運用緩存也很簡單,可以提前或者随查詢将固定或者可預期改變時間的結果放入緩存,設定合适的過期時間即可。當然這麼簡單的手段,優化過一次的接口肯定早就用上了(淦,我就知道沒有那麼簡單!),剩下的都是要求實時查詢的SQL。

SQL優化

SQL優化這部分我已經講過太多了,SQL語句優化可以看從零開始的SQL修煉手冊-實戰篇(32收藏),設計思維和硬體優化看口語化講解資料庫優化(11收藏)。但從我個人角度而言,因為我是做的供應鍊業務,主要是去簡化ERP上的邏輯,定制化開發。受限于一些固定的表結構,導緻很容易有大SQL,比如兩三百行那種,各種聯查子查詢。你們懂的,反正在我這,這種複雜SQL,是成本效益很低的優化操作,我一般選擇不動,玩玩索引得了。

List轉Map

經典空間換時間的優化手段,在算法中比較常用,當然實際開發中也是必要的優化手段。這個手段呢,我之前有篇文章性能優化-如何爽玩多線程來開發(72收藏)講過,和同僚也講過,算是我比較喜歡的一種優化方式。先說說好處,最大的好處就是減少資料庫查詢或者外部接口調用次數,比如你用參數A+B查詢資料庫,循環調用10次就有10次查詢資料庫。但你換個角度,一次性全查出來然後轉換為Map,将參數A+B當作Key從Map裡取,那就少了9次資料庫查詢,這個性能提升是非常巨大的。

當然不可忽視的還有較大的難度,但隻要做好準備,還是比較容易的,還記得前面提到的優化梳理嗎?資料源類的代碼拆分出來後,需要了解SQL的内容,如何修改SQL去一次性查出想要的資料是優化的第一步。舉個例子,這裡有個方法耗時12秒,裡面每個SQL其實查詢不慢,也就1秒,這種SQL優化還有意義嗎?肯定沒有啊,壓縮成幾百毫秒,積少成多也還是壞事啊。是以要改SQL,将這10次查詢結果壓縮成一次查詢。

查詢接口性能優化實錄,講點新手也能用的

比較幸運的是隻有一個條件,那麼簡單粗暴的改成IN查詢即可。

查詢接口性能優化實錄,講點新手也能用的

同時因為擷取了全量的資料,要特别注意記憶體占用的問題,避免發生OOM。常見優化手段有,減少資料傳輸,傳回指定字段,并用精簡後字段的包裝類去接SQL結果,而不是直接拿實體類去接。之前有個OOM的問題就是幾十萬的資料,本來也不多,就那麼幾個有效字段,結果拿一個有一百多個字段的實體類去接,最後占用了1G記憶體。最後定位到問題,解決也簡單,換個包裝類隻接有效字段,記憶體占用一下就降下來了。

查詢接口性能優化實錄,講點新手也能用的

List轉Map這裡我比較喜歡直接用JDK8的Stream流來轉換,這裡value注意一下,不一定給整個實體類,按需即可。比如我這裡就是簡單的産品名稱對應數量,那麼value用數量就行了,細節該注意還是注意一下。

查詢接口性能優化實錄,講點新手也能用的

最後肯定是要改一下代碼的,按照如下圖的思路去做變更即可。提一嘴,前期梳理準備很重要,不然你都不知道在哪改這些代碼。

查詢接口性能優化實錄,講點新手也能用的

多線程

提升性能的不二法寶,上面List轉Map主要針對的是能進行改造的SQL。所謂能改造的SQL是說,查出來的全量資料大小合适,并且本身壓縮後的SQL也快。那麼如果有大量聯查,打死也壓縮不了咋整呢,來人啊,上多線程。傳世經典性能優化-如何爽玩多線程來開發(72收藏)還提到了另外三種法寶,分别是并行聚合處理資料、修改for循環為并行操作、修改Map周遊為并行操作。

并行聚合處理資料主要運用CompletableFuture.allOf()方法,将原本串行的操作改為并行,來壓縮多階段計算的總時間。修改for循環為并行操作主要針對查詢資料庫或者調外部接口這種大量IO的場景。修改Map周遊為并行操作則是為了提升Map的周遊效率,和修改for循環同理。

微醺碼頭

來碼頭整點薯條,休息一下。以上為通用查詢接口的常用優化手段,差不多能覆寫八九成的開發情況了,接下來會擴充一下相關知識點。

請求合并

将多個請求合并,統一處理後傳回結果,簡單來看,就是将一個接口改成了邏輯上支援批量請求。一般是高并發情況下才會用到,并且要求調用方能支援傳回後的統一處理結果,是以盡量能不用就不用。

實作的話,推薦用架構比如hystrix,這種會比較有保障。如果自己弄得話,可以用ConcurrentLinkedQueue作為收集請求的隊列,CompletableFuture來控制請求的調用,newScheduledThreadPool來定時統一執行合并後的請求,這個我就不貼具體實作代碼了,網上挺多。

叢集計算代替單機

典型堆硬體産生質變的例子,比如将複雜的計算分片拆分到叢集的各個機器上,最後彙總計算結果傳回即可。最大化利用機器,這個分片思路其實蠻常見的,比如用elastic-job分布式任務排程這個定時架構裡,就很明顯的在入參處傳遞了分片。

好文推薦

看了不少,田螺哥的這篇實戰總結!18種接口優化方案的總結,總結的比較全面吧。阿裡開發者的淺談系統性能提升的經驗和方法則從更大的角度去闡述了性能優化這個大子產品。後期我也會寫一篇性能優化相關的文章,帶有我的一些了解和實戰,敬請期待嗷。

還有個事說一下,大家可能比較關心,最近如何挖掘項目中的亮點(多方向帶案例)(104收藏)又火起來了,是以本文也被總結成一段亮點追加到文章中了,歡迎大家跳轉觀看。

寫在最後

哈哈,酣暢淋漓,寫完JVM之後再寫我喜歡的就是爽快!本文依舊是為大家帶來幸福快樂的裝逼小技巧,為了完成人人裝逼的偉大夢想,特意用相對口語化的表達方式(也是我最喜歡的)完成了本篇。說回正題,本篇是查詢接口的性能優化,是針對單一場景的優化技巧。當然其他的地方也可以參考使用,畢竟優化的思路都是相通的。

再來說說近況,依舊是鴨梨山大┗|`O′|┛ 嗷~,哎,不過沒辦法,現實這個磨人的小妖精真是讓人欲罷不能。下一篇不出意外會是口語化系列,講MySQL,手裡還攥着一篇重構系統的素材,如果順利的話,下周或者下下周出吧。最後的最後,還是祝大家身體健康、工作順利,這裡是願世界開滿幸福快樂之花的Java菜恐龍,下周見啦~

繼續閱讀