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