我用兩個方法就將接口響應時間從2s優化到了100ms
一、背景
事情的背景就是產品初期,需求急、周期短,同事在完成該需求時以結果導向為主,先實現需求。
就在最近,測試時發現該接口的響應為 2s 多,頁面中可以感覺到明顯的延遲,所以有了本文,提升一下接口響應速度。
在工作中一定要有產品思維,站在用戶的角度,怎么做才能更好。只有當自己站在一個更高的層面,才不會讓你受制于當下,并助你突破現狀。
先說一下目前的代碼邏輯,概括一下就是最外層一個循環,然后循環里面查詢數據庫,而且是一次循環最少 4 次數據庫查詢。
- 根據查詢條件獲取一個總的 list。
- 遍歷該 list,讀取每個對象中的屬性值。
- 根據每個對象屬性值再去數據庫中獲取該子對象。
- 封裝返回數據。
上述代碼是根據實際業務改造的,實際業務還是個嵌套的循環,就拿上圖中的代碼為例,如果讓你來優化,你想選擇哪些優化方式呢?歡迎評論區交流一下。
二、接口優化
先說下我對于接口提升響應速度解決思路吧。首先想到的是定位代碼哪個位置慢,其次慢的原因是什么,最后選擇合適的方案解決問題。
- 使用 Arthas 定位代碼處理慢的點。
- 選擇適合的優化方案。
- 測試優化結果。
對于 Arthas 不了解的可以自己擴展學一下,這里不做過多介紹,只說一句,這是個在線解決生產問題的 Java 診斷工具。
1.定位
對于這個 test 方法我是這樣做的,Idea 中有個 Arthas 插件,所以我們直接在 Idea 中復制對應的命令,粘貼到 Arthas 中進行監控,此處使用的是 trace 命令。
test 方法名位置,右鍵選擇 Arthas Command 中的 Trace 或者 Trace Multiple Class Method Trace -E 命令進行復制。
將該命令粘貼到 Arthas 的終端之后,發起對該接口的請求,即可看到每一行代碼花費的時間,此處我拿一個其他接口來做示例,參考如下:
在這個圖片中,我們重點關注紅框起來的位置,此處打印了該處代碼處理時間的占比,占比越高處理時長越長。所以我們只需要關注一下大頭,也就是占比多的代碼位置進行優化,該接口的響應速度肯定可以提升。
上圖中代碼位置已經脫密處理,在實際的接口調用棧打印中,每一行輸出的末尾都會有代碼所在行數。
2.分析
通過上圖中使用 Arthas 工具的定位,已經知道了方法中哪一塊是處理比較慢的,現在只需要梳理業務邏輯,進行優化即可了。
就以文章開頭的 test 方法舉例,我的優化方案如下:
- 循環遍歷時多次查詢數據庫進行合并,盡量減少數據庫鏈接查詢次數,調用數據庫查詢代碼移動到循環外。
- 根據遍歷的對象屬性獲取數據庫中對應信息時,改為 in 查詢,多條 SQL 合并為一條 SQL。
- 根據查詢 SQL 中使用的參數,創建對應的索引,并確保索引生效。
所以簡單概括優化方案就是:使用 IN 查詢、避免循環查詢數據庫、增加索引三種方式。
修改之后的代碼結構如下:
- 根據查詢條件獲取基礎對象。
- 取出 list 中某一個值例如 aid,生成新 list,當作下一次查詢的查詢條件。
- 封裝對應數據。
最后就是根據查詢條件增加索引,這個本文就不再詳細說明了。
3.小結
通過使用 In 查詢,減少循環中調用數據庫,創建索引等手段,再次請求同一個接口,實現了 2s 到 100ms 的優化。其中索引加入之后,速度快了一倍達到 1s 左右,再通過業務邏輯重構最終實現 100ms 的接口響應速度。
留個討論題,上面優化完成的代碼再讓你進行優化,你還可以在哪些方面進行優化呢,或者說上面代碼可能會留下哪些坑呢?
三、接口常用優化方式
上述的優化方式可以總結為加索引,業務代碼重構兩個方式。其中減少循環調用數據庫是業務代碼重構的內容之一。
數據庫層面,加索引需要注意的是索引是否生效,是否選錯索引等,如果你對 SQL 優化感興趣,點個關注,后續更新一篇 SQL 優化的小技巧。
除了加索引,當數據量起來之后,常用的優化方式還有分庫分表、數據異構、緩存。
1.分庫分表
當系統發展到一定程度之后,用戶的并發量大,會帶來大量的數據庫請求,占用大量的數據庫連接,同時會帶來磁盤IO等方面的問題。
并且隨著系統的長時間使用,產生的數據越來越多,造成單表數據量過大,最終造成查詢緩慢,即使加了索引查詢速度也非常耗時,此時我們就可以使用分庫分表的方式進行數據庫層的優化。
分庫分表大部分同學應該都聽過了,這里大概介紹一下。(點個關注,后續深入分析一下分庫分表)
- 分庫分表有水平與垂直之分,垂直就是業務方向的拆分,水平就是數據方向。
- 分庫解決的是數據庫連接資源不足的問題。
- 分表解決的是單表數據量過大,SQL 語句即使走了索引查詢也非常耗時的問題。
- 在某些業務場景中,用戶并發量大,但是保存的數據量很少時,可以只分庫,不分表。
- 用戶并發量不大,但是保存的數據量很多,可以只分表,不分庫。
- 當用戶并發量大,數據量也很多時,可以考慮分庫分表。
圖中將用戶庫分為3個,在請求到來的時候,可以根據用戶ID進行路由到某一個庫,然后再定位到某張表。路由規則是可以自己定義的。
2.數據異構
數據異構相當于數據進行冗余,在業務接口中,一般返回前端的數據是需要進行多個數據進行封裝的。舉個例子,用戶信息返回,在返回時封裝用戶的部門信息,角色信息一起返回。
現在我們在封裝好上述數據之后,存入 Redis中,只需要讀取一次Redis即可。
3.緩存
緩存相當于把當前請求的響應結果進行緩存,再次讀取時只需要讀取緩存,無需復雜的業務邏輯。比如菜單樹這種數據。
不管是數據異構還是加緩存,都有可能產生數據不一致問題,而MySQL 與Redis如何保持數據一致可以參考之前寫的這篇MySQL與Redis緩存一致性的實現與挑戰。
除了上述的數據方面進行優化外,還可以對代碼進行業務邏輯的重構,或者說是在代碼層面進行優化。常用的方式有異步、避免大事務、減小鎖粒度等。
4.異步
異步的方式有很多,使用@Async注解,線程池或者MQ都可以實現異步。我們關注的重點是哪些業務可以使用異步處理,在業務邏輯處理中,保留核心業務邏輯,非核心業務邏輯異步處理。
在上面這個接口邏輯中,可以發現除了核心業務外,遠程調用,記錄日志都可以放入到異步線程中執行,這樣就無需主線程等待。不過對于遠程調用有的需要返回結果,核心業務需要返回結果支持的另說,這些細節需要好好設計一番了。
5.避免大事務
在使用 @Transaction 注解時,需要注意避免在類上使用該注解,在方法上使用該注解時也要注意方法邏輯不要過于復雜。
大事務可能引發問題如下:
- 死鎖。
- 接口超時。
- 并發情況下數據庫連接池資源耗盡。
- 回滾時間長。
為了避免大事務的產生,在開發時可以注意以下幾點。
- select 查詢方法拿到事務外。
- 避免在事務中進行遠程調用。
- 事務中避免一次處理太多數據,可以拆分為多個小事務。
- 個別功能可以異步處理或者非事務運行。
6.減小鎖粒度
有的場景中,需要進行加鎖操作,防止多線程并發修改,造成數據異常。
但是鎖要是加的不好,還不如不加,如果鎖的粒度太粗,非常影響接口性能。
(1) synchronized
Java 中的關鍵字加鎖,可以寫在方法上或者代碼塊上,舉個例子講一下如何減小鎖粒度。
public synchronized saveFile(String filePath) {
mkdir(filePath);
uploadFile(filePath);
saveMessage(filePath);
}
這里加鎖是為了防止多次觸發創建文件報錯,影響業務。
上傳文件的操作中,隨著文件越來越大,耗費的時間也越長。(此處分片上傳不考慮)
這三個過程放入一個方法中,當前鎖定的就是這三個操作。
所以我們修改一下代碼。
public void saveFile(String filePath) {
synchronized(this) {
if(!exists(filePath)) {
mkdir(filePath);
}
}
uploadFile(filePath);
saveMessage(filePath);
}
修改之后鎖定范圍僅限于創建文件夾,對于上傳文件這種耗時的操作將不再持有鎖,提升接口性能。
(2) Redis 分布式鎖
使用 Redis 實現分布式鎖與 Synchronized 類似,在編寫代碼時盡量控制鎖的范圍,鎖的粒度越小越好。
Redis 實現分布式鎖雖說好用,但是還有8個坑,如果你還不知道可以看下之前寫的這篇文章。
(3) 數據庫鎖
MySQL 為例,鎖有表鎖、行鎖之分,這里主要說這兩種鎖。
表鎖:加鎖快,鎖的粒度最大,發生沖突的概率最高,并發度最低。
行鎖:加鎖慢,會出現死鎖現象,但是鎖的粒度小,發生鎖沖突的概率低,并發度也是最高的。
所以在數據庫鎖的優化方向上,優先行鎖,其次表鎖。
有的同學可能會說還有間隙鎖等等,那些本文就不再多說了,大部分場景下使用行鎖,盡量不使用表鎖即可。
四、總結
在本文中,我們深入探討了接口性能優化的多種策略,從技術細節到實踐應用,講解了接口優化的9種方式:
- 加索引。
- 禁止嵌套for循環。
- 分庫分表。
- 數據異構。
- 緩存。
- 異步。
- 避免大事務。
- 減小鎖粒度。
- 優化SQL。
通過這些優化措施,我們可以顯著提高接口的響應速度和系統的整體性能,為用戶提供更流暢的體驗。