从11s到170ms!看看人家的接口优化技巧,那叫一个优雅!
點擊上方“芋道源碼”,選擇“設(shè)為星標”
管她前浪,還是后浪?
能浪的浪,才是好浪!
每天 10:33?更新文章,每天掉億點點頭發(fā)...
源碼精品專欄
?- 原創(chuàng) | Java 2021?超神之路,很肝~ 
- 中文詳細注釋的開源項目 
- RPC 框架 Dubbo 源碼解析 
- 網(wǎng)絡應用框架 Netty 源碼解析 
- 消息中間件 RocketMQ 源碼解析 
- 數(shù)據(jù)庫中間件 Sharding-JDBC 和 MyCAT 源碼解析 
- 作業(yè)調(diào)度中間件 Elastic-Job 源碼解析 
- 分布式事務中間件 TCC-Transaction 源碼解析 
- Eureka 和 Hystrix 源碼解析 
- Java 并發(fā)源碼 
來源:撿田螺的小男孩
- 前言 
- 1. 批量思想:批量操作數(shù)據(jù)庫 
- 2. 異步思想:耗時操作,考慮放到異步執(zhí)行 
- 3. 空間換時間思想:恰當使用緩存。 
- 4. 預取思想:提前初始化到緩存 
- 5. 池化思想:預分配與循環(huán)使用 
- 6. 事件回調(diào)思想:拒絕阻塞等待。 
- 7. 遠程調(diào)用由串行改為并行 
- 8. 鎖粒度避免過粗 
- 9. 切換存儲方式:文件中轉(zhuǎn)暫存數(shù)據(jù) 
- 10. 索引 
- 11. 優(yōu)化SQL 
- 12.避免大事務問題 
- 13. 深分頁問題 
- 14. 優(yōu)化程序結(jié)構(gòu) 
- 15. 壓縮傳輸內(nèi)容 
- 16. 海量數(shù)據(jù)處理,考慮NoSQL 
- 17. 線程池設(shè)計要合理 
- 18.機器問題 (fullGC、線程打滿、太多IO資源沒關(guān)閉等等)。 
- 最后 
前言
之前工作中,遇到一個504超時問題。原因是因為接口耗時過長,超過nginx配置的10秒。然后 真槍實彈搞了一次接口性能優(yōu)化,最后接口從11.3s降為170ms。本文將跟小伙伴們分享接口優(yōu)化的一些通用 方案。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro 
- 視頻教程:https://doc.iocoder.cn/video/ 
1. 批量思想:批量操作數(shù)據(jù)庫
優(yōu)化前:
//for循環(huán)單筆入庫 for(TransDetail?detail:transDetailList){insert(detail);?? }優(yōu)化后:
batchInsert(transDetailList);打個比喻:
打個比喻:假如你需要搬一萬塊磚到樓頂,你有一個電梯,電梯一次可以放適量的磚(最多放500), 你可以選擇一次運送一塊磚,也可以一次運送500,你覺得哪種方式更方便,時間消耗更少?
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/yudao-cloud 
- 視頻教程:https://doc.iocoder.cn/video/ 
2. 異步思想:耗時操作,考慮放到異步執(zhí)行
耗時操作,考慮用異步處理 ,這樣可以降低接口耗時。
假設(shè)一個轉(zhuǎn)賬接口,匹配聯(lián)行號,是同步執(zhí)行的,但是它的操作耗時有點長 ,優(yōu)化前的流程:
為了降低接口耗時,更快返回,你可以把匹配聯(lián)行號 移到異步處理 ,優(yōu)化后:
- 除了轉(zhuǎn)賬這個例子,日常工作中還有很多這種例子。比如:用戶注冊成功后,短信郵件通知,也是可以異步處理的 ~ 
- 至于異步的實現(xiàn)方式,你可以用線程池,也可以用消息隊列實現(xiàn) 。 
3. 空間換時間思想:恰當使用緩存。
在適當?shù)臉I(yè)務場景,恰當?shù)厥褂镁彺?#xff0c;是可以大大提高接口性能的。緩存其實就是一種空間換時間的思想 ,就是你把要查的數(shù)據(jù),提前放好到緩存里面,需要時,直接查緩存,而避免去查數(shù)據(jù)庫或者計算的過程 。
這里的緩存包括:Redis緩存,JVM本地緩存,memcached,或者Map等等。我舉個我工作中,一次使用緩存優(yōu)化的設(shè)計吧,比較簡單,但是思路很有借鑒的意義。
那是一次轉(zhuǎn)賬接口的優(yōu)化,老代碼 ,每次轉(zhuǎn)賬,都會根據(jù)客戶賬號,查詢數(shù)據(jù)庫,計算匹配聯(lián)行號。
因為每次都查數(shù)據(jù)庫,都計算匹配,比較耗時 ,所以使用緩存 ,優(yōu)化后流程如下:
4. 預取思想:提前初始化到緩存
預取思想很容易理解,就是提前把要計算查詢的數(shù)據(jù),初始化到緩存 。如果你在未來某個時間需要用到某個經(jīng)過復雜計算的數(shù)據(jù),才實時去計算的話,可能耗時比較大 。這時候,我們可以采取預取思想,提前把將來可能需要的數(shù)據(jù)計算好,放到緩存中 ,等需要的時候,去緩存取就行。這將大幅度提高接口性能。
我記得以前在第一個公司做視頻直播的時候,看到我們的直播列表就是用到這種優(yōu)化方案 。就是啟動個任務,提前把直播用戶、積分等相關(guān)信息,初始化到緩存 。
5. 池化思想:預分配與循環(huán)使用
大家應該都記得,我們?yōu)槭裁葱枰褂镁€程池 ?
線程池可以幫我們管理線程,避免增加創(chuàng)建線程和銷毀線程的資源損耗。
如果你每次需要用到線程,都去創(chuàng)建,就會有增加一定的耗時,而線程池可以重復利用線程,避免不必要的耗時。 池化技術(shù)不僅僅指線程池,很多場景都有池化思想的體現(xiàn),它的本質(zhì)就是預分配與循環(huán)使用 。
比如TCP三次握手,大家都很熟悉吧,它為了減少性能損耗,引入了Keep-Alive長連接,避免頻繁的創(chuàng)建和銷毀連接。當然,類似的例子還有很多,如數(shù)據(jù)庫連接池、HttpClient連接池。
我們寫代碼的過程中,學會池化思想 ,最直接相關(guān)的就是使用線程池而不是去new一個線程。
6. 事件回調(diào)思想:拒絕阻塞等待。
如果你調(diào)用一個系統(tǒng)B的接口,但是它處理業(yè)務邏輯,耗時需要10s甚至更多。然后你是一直阻塞等待,直到系統(tǒng)B的下游接口返回 ,再繼續(xù)你的下一步操作嗎?這樣顯然不合理 。
我們參考IO多路復用模型 。即我們不用阻塞等待系統(tǒng)B的接口,而是先去做別的操作。等系統(tǒng)B的接口處理完,通過事件回調(diào) 通知,我們接口收到通知再進行對應的業(yè)務操作即可。
7. 遠程調(diào)用由串行改為并行
假設(shè)我們設(shè)計一個APP首頁的接口,它需要查用戶信息、需要查banner信息、需要查彈窗信息等等。如果是串行一個一個查,比如查用戶信息200ms,查banner信息100ms、查彈窗信息50ms,那一共就耗時350ms了,如果還查其他信息,那耗時就更大了。
其實我們可以改為并行調(diào)用,即查用戶信息、查banner信息、查彈窗信息,可以同時并行發(fā)起 。
最后接口耗時將大大降低 。
8. 鎖粒度避免過粗
在高并發(fā)場景,為了防止超賣等情況 ,我們經(jīng)常需要加鎖來保護共享資源 。但是,如果加鎖的粒度過粗,是很影響接口性能的。
什么是加鎖粒度呢?
其實就是就是你要鎖住的范圍是多大。比如你在家上衛(wèi)生間,你只要鎖住衛(wèi)生間就可以了吧 ,不需要將整個家都鎖起來不讓家人進門吧,衛(wèi)生間就是你的加鎖粒度。
不管你是synchronized加鎖還是redis分布式鎖,只需要在共享臨界資源加鎖即可,不涉及共享資源的,就不必要加鎖。這就好像你上衛(wèi)生間,不用把整個家都鎖住,鎖住衛(wèi)生間門就可以了。
比如,在業(yè)務代碼中,有一個ArrayList因為涉及到多線程操作,所以需要加鎖操作,假設(shè)剛好又有一段比較耗時的操作(代碼中的slowNotShare方法)不涉及線程安全問題。反例加鎖,就是一鍋端,全鎖住 :
//不涉及共享資源的慢方法 private?void?slowNotShare()?{try?{TimeUnit.MILLISECONDS.sleep(100);}?catch?(InterruptedException?e)?{} }//錯誤的加鎖方法 public?int?wrong()?{long?beginTime?=?System.currentTimeMillis();IntStream.rangeClosed(1,?10000).parallel().forEach(i?->?{//加鎖粒度太粗了,slowNotShare其實不涉及共享資源synchronized?(this)?{slowNotShare();data.add(i);}});log.info("cosume?time:{}",?System.currentTimeMillis()?-?beginTime);return?data.size(); }正例:
public?int?right()?{long?beginTime?=?System.currentTimeMillis();IntStream.rangeClosed(1,?10000).parallel().forEach(i?->?{slowNotShare();//可以不加鎖//只對List這部分加鎖synchronized?(data)?{data.add(i);}});log.info("cosume?time:{}",?System.currentTimeMillis()?-?beginTime);return?data.size(); }9. 切換存儲方式:文件中轉(zhuǎn)暫存數(shù)據(jù)
如果數(shù)據(jù)太大,落地數(shù)據(jù)庫實在是慢的話,就可以考慮先用文件的方式暫存 。先保存文件,再異步下載文件,慢慢保存到數(shù)據(jù)庫 。
這里可能會有點抽象,給大家分享一個,我之前的一個真實的優(yōu)化案例 吧。
之前開發(fā)了一個轉(zhuǎn)賬接口。如果是并發(fā)開啟,10個并發(fā)度,每個批次1000筆轉(zhuǎn)賬明細數(shù)據(jù),數(shù)據(jù)庫插入會特別耗時,大概6秒左右 ;這個跟我們公司的數(shù)據(jù)庫同步機制有關(guān),并發(fā)情況下,因為優(yōu)先保證同步,所以并行的插入變成串行啦,就很耗時。
優(yōu)化前 ,1000筆明細轉(zhuǎn)賬數(shù)據(jù),先落地DB數(shù)據(jù)庫,返回處理中給用戶,再異步轉(zhuǎn)賬。如圖:
記得當時壓測的時候,高并發(fā)情況,這1000筆明細入庫,耗時都比較大。所以我轉(zhuǎn)換了一下思路,把批量的明細轉(zhuǎn)賬記錄保存的文件服務器,然后記錄一筆轉(zhuǎn)賬總記錄到數(shù)據(jù)庫即可 。接著異步再把明細下載下來,進行轉(zhuǎn)賬和明細入庫。最后優(yōu)化后,性能提升了十幾倍 。
優(yōu)化后 ,流程圖如下:
如果你的接口耗時瓶頸就在數(shù)據(jù)庫插入操作這里 ,用來批量操作等,還是效果還不理想,就可以考慮用文件或者MQ等暫存。有時候批量數(shù)據(jù)放到文件,會比插入數(shù)據(jù)庫效率更高。
10. 索引
提到接口優(yōu)化,很多小伙伴都會想到添加索引 。沒錯,添加索引是成本最小的優(yōu)化 ,而且一般優(yōu)化效果都很不錯。
索引優(yōu)化這塊的話,一般從這幾個維度去思考:
- 你的SQL加索引了沒? 
- 你的索引是否真的生效? 
- 你的索引建立是否合理? 
10.1 SQL沒加索引
我們開發(fā)的時候,容易疏忽而忘記給SQL添加索引。所以我們在寫完SQL的時候,就順手查看一下 explain執(zhí)行計劃。
explain?select?*?from?user_info?where?userId?like?'%123';你也可以通過命令show create table,整張表的索引情況。
show?create?table?user_info;如果某個表忘記添加某個索引,可以通過alter table add index命令添加索引
alter?table?user_info?add?index?idx_name?(name);一般就是:SQL的where條件的字段,或者是order by 、group by后面的字段需需要添加索引。
10.2 索引不生效
有時候,即使你添加了索引,但是索引會失效的。田螺哥整理了索引失效的常見原因 :
10.3 索引設(shè)計不合理
我們的索引不是越多越好,需要合理設(shè)計。比如:
- 刪除冗余和重復索引。 
- 索引一般不能超過5個 
- 索引不適合建在有大量重復數(shù)據(jù)的字段上、如性別字段 
- 適當使用覆蓋索引 
- 如果需要使用force index強制走某個索引,那就需要思考你的索引設(shè)計是否真的合理了 
11. 優(yōu)化SQL
處了索引優(yōu)化,其實SQL還有很多其他有優(yōu)化的空間。比如這些:
12.避免大事務問題
為了保證數(shù)據(jù)庫數(shù)據(jù)的一致性,在涉及到多個數(shù)據(jù)庫修改 操作時,我們經(jīng)常需要用到事務。而使用spring聲明式事務,又非常簡單,只需要用一個注解就行@Transactional,如下面的例子:
@Transactional public?int?createUser(User?user){//保存用戶信息userDao.save(user);passCertDao.updateFlag(user.getPassId());return?user.getUserId(); }這塊代碼主要邏輯就是創(chuàng)建個用戶,然后更新一個通行證pass的標記。如果現(xiàn)在新增一個需求,創(chuàng)建完用戶,調(diào)用遠程接口發(fā)送一個email消息通知,很多小伙伴會這么寫:
@Transactional public?int?createUser(User?user){//保存用戶信息userDao.save(user);passCertDao.updateFlag(user.getPassId());sendEmailRpc(user.getEmail());return?user.getUserId(); }這樣實現(xiàn)可能會有坑,事務中嵌套RPC遠程調(diào)用,即事務嵌套了一些非DB操作。如果這些非DB操作耗時比較大的話,可能會出現(xiàn)大事務問題 。
所謂大事務問題就是,就是運行時間長的事務 。由于事務一致不提交,就會導致數(shù)據(jù)庫連接被占用,即并發(fā)場景下,數(shù)據(jù)庫連接池被占滿,影響到別的請求訪問數(shù)據(jù)庫,影響別的接口性能 。
大事務引發(fā)的問題主要有:接口超時、死鎖、主從延遲 等等。因此,為了優(yōu)化接口,我們要規(guī)避大事務問題。我們可以通過這些方案來規(guī)避大事務:
- RPC遠程調(diào)用不要放到事務里面 
- 一些查詢相關(guān)的操作,盡量放到事務之外 
- 事務中避免處理太多數(shù)據(jù) 
13. 深分頁問題
在以前公司分析過幾個接口耗時長的問題,最終結(jié)論都是因為深分頁問題 。
深分頁問題,為什么會慢?我們看下這個SQL
select?id,name,balance?from?account?where?create_time>?'2020-09-19'?limit?100000,10;limit 100000,10意味著會掃描100010行,丟棄掉前100000行,最后返回10行。即使create_time,也會回表很多次。
我們可以通過標簽記錄法和延遲關(guān)聯(lián)法 來優(yōu)化深分頁問題。
13.1 標簽記錄法
就是標記一下上次查詢到哪一條了,下次再來查的時候,從該條開始往下掃描。就好像看書一樣,上次看到哪里了,你就折疊一下或者夾個書簽,下次來看的時候,直接就翻到啦。
假設(shè)上一次記錄到100000,則SQL可以修改為:
select??id,name,balance?FROM?account?where?id?>?100000?limit?10;這樣的話,后面無論翻多少頁,性能都會不錯的,因為命中了id主鍵索引。但是這種方式有局限性:需要一種類似連續(xù)自增的字段。
13.2 延遲關(guān)聯(lián)法
延遲關(guān)聯(lián)法,就是把條件轉(zhuǎn)移到主鍵索引樹,然后減少回表。優(yōu)化后的SQL如下:
select??acct1.id,acct1.name,acct1.balance?FROM?account?acct1?INNER?JOIN?(SELECT?a.id?FROM?account?a?WHERE?a.create_time?>?'2020-09-19'?limit?100000,?10)?AS?acct2?on?acct1.id=?acct2.id;優(yōu)化思路就是 ,先通過idx_create_time二級索引樹查詢到滿足條件的主鍵ID,再與原表通過主鍵ID內(nèi)連接,這樣后面直接走了主鍵索引了,同時也減少了回表。
14. 優(yōu)化程序結(jié)構(gòu)
優(yōu)化程序邏輯、程序代碼,是可以節(jié)省耗時的。比如,你的程序創(chuàng)建多不必要的對象、或者程序邏輯混亂,多次重復查數(shù)據(jù)庫、又或者你的實現(xiàn)邏輯算法不是最高效的 ,等等。
我舉個簡單的例子:復雜的邏輯條件,有時候調(diào)整一下順序,就能讓你的程序更加高效。
假設(shè)業(yè)務需求是這樣:如果用戶是會員,第一次登陸時,需要發(fā)一條感謝短信。如果沒有經(jīng)過思考,代碼直接這樣寫了
if(isUserVip?&&?isFirstLogin){sendSmsMsg(); }假設(shè)有5個請求過來,isUserVip判斷通過的有3個請求,isFirstLogin通過的只有1個請求。那么以上代碼,isUserVip執(zhí)行的次數(shù)為5次,isFirstLogin執(zhí)行的次數(shù)也是3次,如下:
如果調(diào)整一下isUserVip和isFirstLogin的順序:
if(isFirstLogin?&&?isUserVip?){sendMsg(); }isFirstLogin執(zhí)行的次數(shù)是5次,isUserVip執(zhí)行的次數(shù)是1次:
醬紫程序是不是變得更高效了呢?
15. 壓縮傳輸內(nèi)容
壓縮傳輸內(nèi)容,傳輸報文變得更小,因此傳輸會更快啦。10M帶寬,傳輸10k的報文,一般比傳輸1M的會快呀。
打個比喻,一匹千里馬,它馱著100斤的貨跑得快,還是馱著10斤的貨物跑得快呢?
再舉個視頻網(wǎng)站的例子:
如果不對視頻做任何壓縮編碼,因為帶寬又是有限的。巨大的數(shù)據(jù)量在網(wǎng)絡傳輸?shù)暮臅r會比編碼壓縮后,慢好多倍 。
16. 海量數(shù)據(jù)處理,考慮NoSQL
之前看過幾個慢SQL,都是跟深分頁問題有關(guān)的。發(fā)現(xiàn)用來標簽記錄法和延遲關(guān)聯(lián)法,效果不是很明顯 ,原因是要統(tǒng)計和模糊搜索,并且統(tǒng)計的數(shù)據(jù)是真的大。最后跟組長對齊方案,就把數(shù)據(jù)同步到Elasticsearch,然后這些模糊搜索需求,都走Elasticsearch去查詢了。
我想表達的就是,如果數(shù)據(jù)量過大,一定要用關(guān)系型數(shù)據(jù)庫存儲的話,就可以分庫分表。但是有時候,我們也可以使用NoSQL,如Elasticsearch、Hbase等。
17. 線程池設(shè)計要合理
我們使用線程池,就是讓任務并行處理,更高效地完成任務 。但是有時候,如果線程池設(shè)計不合理,接口執(zhí)行效率則不太理想。
一般我們需要關(guān)注線程池的這幾個參數(shù):核心線程、最大線程數(shù)量、阻塞隊列 。
- 如果核心線程過小,則達不到很好的并行效果。 
- 如果阻塞隊列不合理,不僅僅是阻塞的問題,甚至可能會OOM 
- 如果線程池不區(qū)分業(yè)務隔離,有可能核心業(yè)務被邊緣業(yè)務拖垮 。 
18.機器問題 (fullGC、線程打滿、太多IO資源沒關(guān)閉等等)。
有時候,我們的接口慢,就是機器處理問題。主要有fullGC、線程打滿、太多IO資源沒關(guān)閉等等。
- 之前排查過一個fullGC問題:運營小姐姐導出60多萬的excel的時候,說卡死 了,接著我們就收到監(jiān)控告警。后面排查得出,我們老代碼是Apache POI生成的excel,導出excel數(shù)據(jù)量很大時,當時JVM內(nèi)存吃緊會直接Full GC了。 
- 如果線程打滿了,也會導致接口都在等待了。所以。如果是高并發(fā)場景,我們需要接入限流,把多余的請求拒絕掉 。 
- 如果IO資源沒關(guān)閉,也會導致耗時增加 。這個大家可以看下,平時你的電腦一直打開很多很多文件,是不是會覺得很卡。 
最后
本文介紹了優(yōu)化接口的18種 方案。如果對你有幫助,麻煩給一個三連(點贊、在看、轉(zhuǎn)發(fā))。一起加油
歡迎加入我的知識星球,一起探討架構(gòu),交流源碼。加入方式,長按下方二維碼噢:
已在知識星球更新源碼解析如下:
最近更新《芋道 SpringBoot 2.X 入門》系列,已經(jīng) 101 余篇,覆蓋了?MyBatis、Redis、MongoDB、ES、分庫分表、讀寫分離、SpringMVC、Webflux、權(quán)限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能測試等等內(nèi)容。
提供近 3W 行代碼的 SpringBoot 示例,以及超 4W 行代碼的電商微服務項目。
獲取方式:點“在看”,關(guān)注公眾號并回復?666?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。 謝謝支持喲 (*^__^*)總結(jié)
以上是生活随笔為你收集整理的从11s到170ms!看看人家的接口优化技巧,那叫一个优雅!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 常见浏览器及其内核(国际)
- 下一篇: java 获取上周开始时间和结束时间,上
