天天說要做效能優化,到底在優化什麼?

猿天地發表於2020-11-02

面試過程中經常被問到:

  • 你做過效能優化嗎?
  • 優化了哪些方面?
  • 怎麼做優化的?
  • 優化的效果如何?

連環炮問下來,對於有做過優化的老司機來說,肯定能抗住。對於沒有真正做過優化的小白來說,肯定扛不住這一系列的追問,最後只能以面試失敗而告終。

那麼效能優化到底在優化什麼呢?我們來盤點下一些常用的優化手段。

SQL 優化

當你開發的介面響應時間超過了 200ms 的時候就得優化了,當然 200ms 不是絕對值,具體還是看應用場景。以 App 舉例,進一個頁面呼叫 5 個介面(題外話:也可以做聚合),那麼總共就是 1s 的時間,對使用者來說體驗還算可以,當然是越快響應越好。

介面耗時 200ms,其中佔大頭的還是對資料庫的操作,一個介面中會有 N 次資料庫操作。所以優化 SQL 的速度優先順序是最高的,大量的慢 SQL 會拖垮整個系統。

關於 SQL 的優化不是本文的重點,大部分慢 SQL 還是跟各位平時開發時的習慣有關係。大部分在寫 SQL 的時候不太會去考慮效能,只要寫出來就可以了,join 隨手就來,也不梳理查詢欄位,不加索引,剛開始上線沒問題,等到併發量,資料量起來的時候就涼涼了。

關於資料庫的使用規範大家可以參考下這篇文章:https://mp.weixin.qq.com/s/mFsK7YSKcG6T7jpPnK92tg

當資料量大了後肯定要做讀寫分離和分庫分表的,這也是優化的必經之路。相關的文章也可以參考我之前寫的一些:http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMDY0Nzk2Mw==&hid=4&sn=1b96093ec951a5f997bdd3225e5f2fdf&scene=18#wechat_redirect

減少重複呼叫

效能不好的另一個致命問題就是重複呼叫,相同的邏輯在不同的方法中重複對資料庫查詢,重複呼叫 RPC 服務等。

比如下面的程式碼:

skuDao.querySkus(productId).stream().map(sku -> {
   skuDao.getById(sku.getId());
})

明明資料已經查詢出來了,又根據 ID 重新去查詢了一次,數量越多,浪費的時間越多。這裡只是舉例,我相信在真實的專案中大量存在重複查詢的情況,之前我還寫過一篇文章,講解如何解決這種重複查詢問題,感興趣的可以檢視這篇文章:https://mp.weixin.qq.com/s/1k4OtNYIoOasrXAF1ZhcGg

按需查詢

很多業務邏輯不復雜的功能,卻響應很慢。往往都是寫程式碼的時候沒有思考,隨便就呼叫一些已經存在的方法,導致整體響應變慢,總結起來就是:效能問題大部分都是程式碼寫出來的

說個場景,大家肯定都見到過。引數是一個商品 ID, 功能是上架商品,需要進行狀態的判斷,符合條件才能上架。這個場景下只需要獲取商品的狀態進行判斷即可,有的時候你看到的程式碼往往都是下面的方式:

GoodsDetail goods = goodsService.detail(id);
if (goods.getStatus() == GoodsStatusEnum.XXXX) {

}

detail 中有大量的邏輯,除了基本的商品資訊,還有很多其他的內容,這就是慢的原因。

並行呼叫

針對一個介面,如果設計到多個內部 RPC 服務或者多個外部介面,在介面之間沒有關聯關係的情況下,我們可以採用並行呼叫的方式來提高效能。

CompletableFuture 就非常適合並行呼叫的場景,關於 CompletableFuture 的使用本文不做詳細說明,做 Java 的都要會用。

除了 CompletableFuture 之外,對於集合類的處理,可以用 parallelStream 來實現並行呼叫。

在微服務中有一層專門用於聚合 API, 聚合層就非常適合並行呼叫,一個功能或者一個頁面展示會涉及到多個介面,通過聚合層在後端進行介面的聚合和資料的裁剪,一起響應給前端。

上快取

快取也是優化中最常用的,效果提升最明顯的,成本也不大。對於快取,也不要濫用,不是所有場景都可以靠堆快取來提高效能的。

首先對於實時性要求不高的業務場景可以優先使用快取,也不用太考慮更新的問題,自然過期就行。

實時性要求高的業務場景,用快取一定要有完整的快取更新機制,否則很容易造成業務資料和快取資料不一致的情況。

建議的做法是訂閱 binlog 來統一更新快取,不要在程式碼中去更新或者失效快取,簡單的場景還好,入口就那幾個,問題不大。有些資料在多個場景下使用,需要更新的入口太多了,

非同步處理

有些邏輯,不需要實時反饋給使用者那就可以採用非同步的方式在後臺進行處理。

非同步處理的方式最常見的就是將任務加到執行緒池中進行處理,執行緒池需要考慮容量以及對一些指標的監控,相關的文章可以檢視我的這篇:https://mp.weixin.qq.com/s/JM9idgFPZGkRAdCpw0NaKw

除了一些指標的監控,執行緒池的使用另一個需要關注的問題就是任務的持久化。如果你的資料本來就是儲存好了的,然後讀取出來通過執行緒池去執行是沒問題的。如果是沒有持久化直接丟入執行緒池中進行執行,就有可能出現丟失的情況,比如服務重啟之類的場景。

關於持久化,無論是執行緒池還是 EventBus 這種,都會遇到,所以針對非同步的場景我建議大家使用訊息佇列比較好。

訊息佇列可以儲存任務資訊,保證不會丟失。單獨消費佇列的訊息進行邏輯處理,如果想提高消費速度,也可以在佇列的消費方使用執行緒池進行多執行緒消費,多執行緒消費也要避免訊息丟失的情況,可以檢視我的這篇文章:https://mp.weixin.qq.com/s/Bbh1GDpmkLhZhw5f0POJ2A

JVM 引數調整

JVM 引數的調整,一般情況下我們都不用怎麼去調整。偶爾有些程式碼寫的不好,導致記憶體溢位了,這個時候會去做一些調整和優化程式碼。

引數調整主要是去降低 GC 的導致的停頓問題,如果你的程式一直在 GC, 一直在停頓,你的介面自然就慢了。

只要沒有頻繁的 Full GC,在優化這塊 JVM 的引數調整可以最後再做,優先以 SQL 優化這些為主。

加機器

加機器是最後的終極大招了,併發量上去的時候,你在怎麼優化單機器和單資料庫抗併發能力也是有限的,這個時候只能水平擴充套件了。

如果是創業初期,並且在快速發展,加機器是最直接的優化方式了,雖然說成本上去了,但是開發資源也是成本,節約下來可以實現更多的業務需求。等到中期穩定了再考慮架構,效能方面整體的優化和重構。

就像玩遊戲一樣,有裝備的玩家才能所向睥睨啊,對於後端應用來說也是一樣,高配的機器,高配的資料庫配置,高配的快取等。

關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。

我整理了一份很全的學習資料,感興趣的可以微信搜尋「猿天地」,回覆關鍵字 「學習資料」獲取我整理好了的 Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC 分庫分表,任務排程框架 XXL-JOB,MongoDB,爬蟲等相關資料。

相關文章