0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

接口性能優(yōu)化的11個小技巧,大家務(wù)必掌握!

jf_ro2CN3Fa ? 來源:芋道源碼 ? 2023-11-17 16:25 ? 次閱讀


前言

接口性能優(yōu)化 對于從事后端開發(fā)的同學(xué)來說,肯定再熟悉不過了,因為它是一個跟開發(fā)語言無關(guān)的公共問題。

該問題說簡單也簡單,說復(fù)雜也復(fù)雜。

有時候,只需加個索引就能解決問題。

有時候,需要做代碼重構(gòu)。

有時候,需要增加緩存。

有時候,需要引入一些中間件,比如 mq。

有時候,需要分庫分表。

有時候,需要拆分服務(wù)。

等等。。。

導(dǎo)致接口性能問題的原因千奇百怪,不同的項目不同的接口,原因可能也不一樣。

本文總結(jié)了一些行之有效的優(yōu)化接口性能的辦法,給有需要的朋友一個參考。

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(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.索引

接口性能優(yōu)化大家第一個想到的可能是:優(yōu)化索引。

沒錯,優(yōu)化索引的成本是最小的。

你通過查看線上日志或者監(jiān)控報告,查到某個接口用到的某條 sql 語句耗時比較長。

這時你可能會有下面這些疑問:

  1. 該 sql 語句加索引了沒?
  2. 加的索引生效了沒?
  3. mysql 選錯索引了沒?

1.1 沒加索引

sql 語句中where條件的關(guān)鍵字段,或者order by后面的排序字段,忘了加索引,這個問題在項目中很常見。

項目剛開始的時候,由于表中的數(shù)據(jù)量小,加不加索引 sql 查詢性能差別不大。

后來,隨著業(yè)務(wù)的發(fā)展,表中數(shù)據(jù)量越來越多,就不得不加索引了。

可以通過命令:

showindexfrom`order`;

能單獨(dú)查看某張表的索引情況。

也可以通過命令:

showcreatetable`order`;

查看整張表的建表語句,里面同樣會顯示索引情況。

通過ALTER TABLE命令可以添加索引:

ALTERTABLE`order`ADDINDEXidx_name(name);

也可以通過CREATE INDEX命令添加索引:

CREATEINDEXidx_nameON`order`(name);

不過這里有一個需要注意的地方是:想通過命令修改索引,是不行的。

目前在 mysql 中如果想要修改索引,只能先刪除索引,再重新添加新的。

刪除索引可以用ALTER TABLE命令:

ALTERTABLE`order`DROPINDEXidx_name;

DROP INDEX命令也行:

DROPINDEXidx_nameON`order`;

1.2 索引沒生效

通過上面的命令我們已經(jīng)能夠確認(rèn)索引是有的,但它生效了沒?此時你內(nèi)心或許會冒出這樣一個疑問。

那么,如何查看索引有沒有生效呢?

答:可以使用explain命令,查看 mysql 的執(zhí)行計劃,它會顯示索引的使用情況。

例如:

explainselect*from`order`wherecode='002';

結(jié)果:

035bb20c-851d-11ee-939d-92fbcf53809c.jpg

通過這幾列可以判斷索引使用情況,執(zhí)行計劃包含列的含義如下圖所示:

037a01c6-851d-11ee-939d-92fbcf53809c.jpg

說實(shí)話,sql語句沒有走索引,排除沒有建索引之外,最大的可能性是索引失效了。

下面說說索引失效的常見原因:

03aa5f42-851d-11ee-939d-92fbcf53809c.jpg

如果不是上面的這些原因,則需要再進(jìn)一步排查一下其他原因。

1.3 選錯索引

此外,你有沒有遇到過這樣一種情況:明明是同一條 sql,只有入?yún)⒉煌?。有的時候走的索引 a,有的時候卻走的索引 b?

沒錯,有時候 mysql 會選錯索引。

必要時可以使用force index來強(qiáng)制查詢 sql 走某個索引。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(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. sql 優(yōu)化

如果優(yōu)化了索引之后,也沒啥效果。

接下來試著優(yōu)化一下 sql 語句,因為它的改造成本相對于 java 代碼來說也要小得多。

下面給大家列舉了 sql 優(yōu)化的 15 個小技巧:

03c2813a-851d-11ee-939d-92fbcf53809c.jpg

3. 遠(yuǎn)程調(diào)用

很多時候,我們需要在某個接口中,調(diào)用其他服務(wù)的接口。

比如有這樣的業(yè)務(wù)場景:

在用戶信息查詢接口中需要返回:用戶名稱、性別、等級、頭像、積分、成長值等信息。

而用戶名稱、性別、等級、頭像在用戶服務(wù)中,積分在積分服務(wù)中,成長值在成長值服務(wù)中。為了匯總這些數(shù)據(jù)統(tǒng)一返回,需要另外提供一個對外接口服務(wù)。

于是,用戶信息查詢接口需要調(diào)用用戶查詢接口、積分查詢接口和成長值查詢接口,然后匯總數(shù)據(jù)統(tǒng)一返回。

調(diào)用過程如下圖所示:

03e5cd48-851d-11ee-939d-92fbcf53809c.png

調(diào)用遠(yuǎn)程接口總耗時 530ms = 200ms + 150ms + 180ms。

顯然這種串行調(diào)用遠(yuǎn)程接口性能是非常不好的,調(diào)用遠(yuǎn)程接口總的耗時為所有的遠(yuǎn)程接口耗時之和。

那么如何優(yōu)化遠(yuǎn)程接口性能呢?

3.1 并行調(diào)用

上面說到,既然串行調(diào)用多個遠(yuǎn)程接口性能很差,為什么不改成并行呢?

如下圖所示:

0408d0fe-851d-11ee-939d-92fbcf53809c.png

調(diào)用遠(yuǎn)程接口總耗時 200ms = 200ms(即耗時最長的那次遠(yuǎn)程接口調(diào)用)。

在 java8 之前可以通過實(shí)現(xiàn)Callable接口,獲取線程返回結(jié)果。java8 以后通過CompleteFuture類實(shí)現(xiàn)該功能。

我們這里以 CompleteFuture 為例:

publicUserInfogetUserInfo(Longid)throwsInterruptedException,ExecutionException{
finalUserInfouserInfo=newUserInfo();
CompletableFutureuserFuture=CompletableFuture.supplyAsync(()->{
getRemoteUserAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);

CompletableFuturebonusFuture=CompletableFuture.supplyAsync(()->{
getRemoteBonusAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);

CompletableFuturegrowthFuture=CompletableFuture.supplyAsync(()->{
getRemoteGrowthAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);
CompletableFuture.allOf(userFuture,bonusFuture,growthFuture).join();

userFuture.get();
bonusFuture.get();
growthFuture.get();

returnuserInfo;
}

溫馨提醒一下,這兩種方式別忘了使用線程池。示例中我用到了 executor,表示自定義的線程池,為了防止高并發(fā)場景下,出現(xiàn)線程過多的問題。

3.2 數(shù)據(jù)異構(gòu)

上面說到的用戶信息查詢接口需要調(diào)用用戶查詢接口、積分查詢接口和成長值查詢接口,然后匯總數(shù)據(jù)統(tǒng)一返回。

那么,我們能不能把數(shù)據(jù)冗余一下,把用戶信息、積分和成長值的數(shù)據(jù)統(tǒng)一存儲到一個地方,比如:redis,存的數(shù)據(jù)結(jié)構(gòu)就是用戶信息查詢接口所需要的內(nèi)容。然后通過用戶 id,直接從 redis 中查詢數(shù)據(jù)出來,不就 OK了?

如果在高并發(fā)的場景下,為了提升接口性能,遠(yuǎn)程接口調(diào)用大概率會被去掉,而改成保存冗余數(shù)據(jù)的數(shù)據(jù)異構(gòu)方案。

041f469a-851d-11ee-939d-92fbcf53809c.png

但需要注意的是,如果使用了數(shù)據(jù)異構(gòu)方案,就可能會出現(xiàn)數(shù)據(jù)一致性問題。

用戶信息、積分和成長值有更新的話,大部分情況下,會先更新到數(shù)據(jù)庫,然后同步到 redis。但這種跨庫的操作,可能會導(dǎo)致兩邊數(shù)據(jù)不一致的情況產(chǎn)生。

4. 重復(fù)調(diào)用

重復(fù)調(diào)用在我們的日常工作代碼中可以說隨處可見,但如果沒有控制好,會非常影響接口的性能。

不信,我們一起看看。

4.1 循環(huán)查數(shù)據(jù)庫

有時候,我們需要從指定的用戶集合中,查詢出有哪些是在數(shù)據(jù)庫中已經(jīng)存在的。

實(shí)現(xiàn)代碼可以這樣寫:

publicListqueryUser(ListsearchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}

Listresult=Lists.newArrayList();
searchList.forEach(user->result.add(userMapper.getUserById(user.getId())));
returnresult;
}

這里如果有 50 個用戶,則需要循環(huán) 50 次去查詢數(shù)據(jù)庫。我們都知道,每查詢一次數(shù)據(jù)庫,就是一次遠(yuǎn)程調(diào)用。

如果查詢 50 次數(shù)據(jù)庫,就有 50 次遠(yuǎn)程調(diào)用,這是非常耗時的操作。

那么,我們?nèi)绾蝺?yōu)化呢?

具體代碼如下:

publicListqueryUser(ListsearchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}
Listids=searchList.stream().map(User::getId).collect(Collectors.toList());
returnuserMapper.getUserByIds(ids);
}

提供一個根據(jù)用戶 id 集合批量查詢用戶的接口,只遠(yuǎn)程調(diào)用一次,就能查詢出所有的數(shù)據(jù)。

這里有個需要注意的地方是:id 集合的大小要做限制,最好一次不要請求太多的數(shù)據(jù)。要根據(jù)實(shí)際情況而定,建議控制每次請求的記錄條數(shù)在 500 以內(nèi)。

4.2 死循環(huán)

有些小伙伴看到這個標(biāo)題,可能會感到有點(diǎn)意外,死循環(huán)也算?

代碼中不是應(yīng)該避免死循環(huán)嗎?為啥還是會產(chǎn)生死循環(huán)?

有時候死循環(huán)是我們自己寫的,例如下面這段代碼:

while(true){
if(condition){
break;
}
System.out.println("dosamething");
}

這里使用了 while(true) 的循環(huán)調(diào)用,這種寫法在CAS自旋鎖中使用比較多。

當(dāng)滿足 condition 等于 true 的時候,則自動退出該循環(huán)。

如果 condition 條件非常復(fù)雜,一旦出現(xiàn)判斷不正確,或者少寫了一些邏輯判斷,就可能在某些場景下出現(xiàn)死循環(huán)的問題。

出現(xiàn)死循環(huán),大概率是開發(fā)人員人為的 bug 導(dǎo)致的,不過這種情況很容易被測出來。

還有一種隱藏的比較深的死循環(huán),是由于代碼寫得不太嚴(yán)謹(jǐn)導(dǎo)致的。如果用正常數(shù)據(jù),可能測不出問題,但一旦出現(xiàn)異常數(shù)據(jù),就會立即出現(xiàn)死循環(huán)。

4.3 無限遞歸

如果想要打印某個分類的所有父分類,可以用類似這樣的遞歸方法實(shí)現(xiàn):

publicvoidprintCategory(Categorycategory){
if(category==null
||category.getParentId()==null){
return;
}
System.out.println("父分類名稱:"+category.getName());
Categoryparent=categoryMapper.getCategoryById(category.getParentId());
printCategory(parent);
}

正常情況下,這段代碼是沒有問題的。

但如果某次有人誤操作,把某個分類的 parentId 指向了它自己,這樣就會出現(xiàn)無限遞歸的情況。導(dǎo)致接口一直不能返回數(shù)據(jù),最終會發(fā)生堆棧溢出。

建議寫遞歸方法時,設(shè)定一個遞歸的深度,比如:分類最大等級有 4 級,則深度可以設(shè)置為 4。然后在遞歸方法中做判斷,如果深度大于 4 時,則自動返回,這樣就能避免無限循環(huán)的情況。

5. 異步處理

有時候,我們接口性能優(yōu)化,需要重新梳理一下業(yè)務(wù)邏輯,看看是否有設(shè)計上不太合理的地方。

比如有個用戶請求接口中,需要做業(yè)務(wù)操作、發(fā)站內(nèi)通知和記錄操作日志。為了實(shí)現(xiàn)起來比較方便,通常我們會將這些邏輯放在接口中同步執(zhí)行,勢必會對接口性能造成一定的影響。

接口內(nèi)部流程圖如下:

043fad86-851d-11ee-939d-92fbcf53809c.png

這個接口表面上看起來沒有問題,但如果你仔細(xì)梳理一下業(yè)務(wù)邏輯,會發(fā)現(xiàn)只有業(yè)務(wù)操作才是核心邏輯,其他的功能都是非核心邏輯

在這里有個原則就是:核心邏輯可以同步執(zhí)行,同步寫庫。非核心邏輯,可以異步執(zhí)行,異步寫庫。

上面這個例子中,發(fā)站內(nèi)通知和用戶操作日志功能,對實(shí)時性要求不高,即使晚點(diǎn)寫庫,用戶無非是晚點(diǎn)收到站內(nèi)通知,或者運(yùn)營晚點(diǎn)看到用戶操作日志,對業(yè)務(wù)影響不大,所以完全可以異步處理。

通常異步主要有兩種:多線程mq。

5.1 線程池

使用線程池改造之后,接口邏輯如下:

0458ab92-851d-11ee-939d-92fbcf53809c.png

發(fā)站內(nèi)通知和用戶操作日志功能,被提交到了兩個單獨(dú)的線程池中。

這樣接口中重點(diǎn)關(guān)注的是業(yè)務(wù)操作,把其他的邏輯交給線程異步執(zhí)行,這樣改造之后,讓接口性能瞬間提升了。

但使用線程池有個小問題就是:如果服務(wù)器重啟了,或者是需要被執(zhí)行的功能出現(xiàn)異常了,無法重試,會丟數(shù)據(jù)。

5.2 mq

使用mq改造之后,接口邏輯如下:

04746134-851d-11ee-939d-92fbcf53809c.png

對于發(fā)站內(nèi)通知和用戶操作日志功能,在接口中并沒真正實(shí)現(xiàn),它只發(fā)送了 mq 消息到 mq 服務(wù)器。然后由 mq 消費(fèi)者消費(fèi)消息時,才真正地執(zhí)行這兩個功能。

這樣改造之后,接口性能同樣提升了,因為發(fā)送 mq 消息速度是很快的,我們只需關(guān)注業(yè)務(wù)操作的代碼即可。

6. 避免大事務(wù)

很多小伙伴在使用 spring 框架開發(fā)項目時,為了方便,喜歡使用@Transactional注解提供事務(wù)功能。

沒錯,使用 @Transactional 注解這種聲明式事務(wù)的方式提供事務(wù)功能,確實(shí)能少寫很多代碼,提升開發(fā)效率。

但也容易造成大事務(wù),引發(fā)其他的問題。

下面用一張圖看看大事務(wù)引發(fā)的問題。

049c9bea-851d-11ee-939d-92fbcf53809c.png

從圖中能夠看出,大事務(wù)問題可能會造成接口超時,對接口的性能有直接的影響。

我們該如何優(yōu)化大事務(wù)呢?

  1. 少用 @Transactional 注解
  2. 將查詢(select)方法放到事務(wù)外
  3. 事務(wù)中避免遠(yuǎn)程調(diào)用
  4. 事務(wù)中避免一次性處理太多數(shù)據(jù)
  5. 有些功能可以非事務(wù)執(zhí)行
  6. 有些功能可以異步處理

7. 鎖粒度

在某些業(yè)務(wù)場景中,多個線程并發(fā)修改某個共享數(shù)據(jù),會造成數(shù)據(jù)異常。

為了解決并發(fā)場景下,多個線程同時修改數(shù)據(jù)造成數(shù)據(jù)不一致的情況,通常情況下,我們會:加鎖。

但如果鎖加得不好,導(dǎo)致鎖的粒度太粗,也會非常影響接口性能。

7.1 synchronized

在 java 中提供了synchronized關(guān)鍵字給我們的代碼加鎖。

通常有兩種寫法:在方法上加鎖在代碼塊上加鎖。

先看看如何在方法上加鎖:

publicsynchronizeddoSave(StringfileUrl){
mkdir();
uploadFile(fileUrl);
sendMessage(fileUrl);
}

這里加鎖的目的是為了防止并發(fā)的情況下創(chuàng)建了相同的目錄,第二次會創(chuàng)建失敗,影響業(yè)務(wù)功能。

但這種直接在方法上加鎖,鎖的粒度有點(diǎn)粗。因為 doSave 方法中的上傳文件和發(fā)消息功能,是不需要加鎖的。只有創(chuàng)建目錄功能,才需要加鎖。

我們都知道文件上傳操作是非常耗時的,如果將整個方法加鎖,那么需要等到整個方法執(zhí)行完之后才能釋放鎖。顯然,這會導(dǎo)致該方法的性能很差,變得得不償失。

這時,我們可以改成在代碼塊上加鎖了,具體代碼如下:

publicvoiddoSave(Stringpath,StringfileUrl){
synchronized(this){
if(!exists(path)){
mkdir(path);
}
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}

這樣改造之后,鎖的粒度一下子變小了,只有并發(fā)創(chuàng)建目錄功能才加了鎖。而創(chuàng)建目錄是一個非??斓牟僮?,即使加鎖對接口的性能影響也不大。

最重要的是,其他的上傳文件和發(fā)送消息功能,仍然可以并發(fā)執(zhí)行。

當(dāng)然,這樣做在單機(jī)版的服務(wù)中,是沒有問題的。但現(xiàn)在部署的生產(chǎn)環(huán)境,為了保證服務(wù)的穩(wěn)定性,一般情況下,同一個服務(wù)會被部署在多個節(jié)點(diǎn)中。如果哪天掛了一個節(jié)點(diǎn),其他的節(jié)點(diǎn)服務(wù)仍然可用。

多節(jié)點(diǎn)部署避免了因為某個節(jié)點(diǎn)掛了,導(dǎo)致服務(wù)不可用的情況。同時也能分?jǐn)傉麄€系統(tǒng)的流量,避免系統(tǒng)壓力過大。

同時它也帶來了新的問題:synchronized 只能保證一個節(jié)點(diǎn)加鎖是有效的,但如果有多個節(jié)點(diǎn)如何加鎖呢?

答:這就需要使用:分布式鎖了。目前主流的分布式鎖包括:redis 分布式鎖、zookeeper 分布式鎖和數(shù)據(jù)庫分布式鎖。

由于 zookeeper 分布式鎖的性能不太好,真實(shí)業(yè)務(wù)場景用的不多,這里就不講了。

下面聊一下 redis 分布式鎖。

7.2 redis 分布式鎖

在分布式系統(tǒng)中,由于 redis 分布式鎖相對更簡單和高效,成為了分布式鎖的首選,被我們用到了很多實(shí)際業(yè)務(wù)場景當(dāng)中。

使用 redis 分布式鎖的偽代碼如下:

publicvoiddoSave(Stringpath,StringfileUrl){
try{
Stringresult=jedis.set(lockKey,requestId,"NX","PX",expireTime);
if("OK".equals(result)){
if(!exists(path)){
mkdir(path);
uploadFile(fileUrl);
sendMessage(fileUrl);
}
returntrue;
}
}finally{
unlock(lockKey,requestId);
}
returnfalse;
}

跟之前使用synchronized關(guān)鍵字加鎖時一樣,這里鎖的范圍也太大了,換句話說就是鎖的粒度太粗,這樣會導(dǎo)致整個方法的執(zhí)行效率很低。

其實(shí)只有創(chuàng)建目錄的時候,才需要加分布式鎖,其余代碼根本不用加鎖。

于是,我們需要優(yōu)化一下代碼:

publicvoiddoSave(Stringpath,StringfileUrl){
if(this.tryLock()){
mkdir(path);
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}

privatebooleantryLock(){
try{
Stringresult=jedis.set(lockKey,requestId,"NX","PX",expireTime);
if("OK".equals(result)){
returntrue;
}
}finally{
unlock(lockKey,requestId);
}
returnfalse;
}

上面代碼將加鎖的范圍縮小了,只有創(chuàng)建目錄時才加了鎖。這樣看似簡單的優(yōu)化之后,接口性能能提升很多。說不定,會有意外的驚喜喔。哈哈哈。

redis 分布式鎖雖說好用,但它在使用時,有很多注意的細(xì)節(jié),隱藏了很多坑,如果稍不注意很容易踩中。redis 分布式鎖的 8 大坑具體如下:

04becfbc-851d-11ee-939d-92fbcf53809c.jpg

7.3 數(shù)據(jù)庫分布式鎖

mysql 數(shù)據(jù)庫中主要有三種鎖:

  • 表鎖:加鎖快,不會出現(xiàn)死鎖。但鎖的粒度大,發(fā)生鎖沖突的概率最高,并發(fā)度最低。
  • 行鎖:加鎖慢,會出現(xiàn)死鎖。但鎖的粒度最小,發(fā)生鎖沖突的概率最低,并發(fā)度也最高。
  • 間隙鎖:開銷和加鎖時間界于表鎖和行鎖之間。它會出現(xiàn)死鎖,鎖的粒度界于表鎖和行鎖之間,并發(fā)度一般。

并發(fā)度越高,意味著接口性能越好。

所以數(shù)據(jù)庫鎖的優(yōu)化方向是:優(yōu)先使用行鎖,其次使用間隙鎖,再其次使用表鎖。

趕緊看看,你用對了沒?

8.分頁處理

有時候我們會調(diào)用某個接口批量查詢數(shù)據(jù),比如:通過用戶 id 批量查詢出用戶信息,然后給這些用戶送積分。

但如果你一次性查詢的用戶數(shù)量太多了,比如一次查詢 2000 個用戶的數(shù)據(jù)。參數(shù)中傳入了 2000 個用戶的 id,遠(yuǎn)程調(diào)用接口,會發(fā)現(xiàn)該用戶查詢接口經(jīng)常超時。

調(diào)用代碼如下:

Listusers=remoteCallUser(ids);

眾所周知,調(diào)用接口從數(shù)據(jù)庫獲取數(shù)據(jù),是需要經(jīng)過網(wǎng)絡(luò)傳輸?shù)?。如果?shù)據(jù)量太大,無論是獲取數(shù)據(jù)的速度,還是網(wǎng)絡(luò)傳輸受限于帶寬,都會導(dǎo)致消耗時間比較長。

那么,這種情況要如何優(yōu)化呢?

答:分頁處理。

將一次獲取所有數(shù)據(jù)的請求,改成分多次獲取,每次只獲取一部分用戶的數(shù)據(jù),最后進(jìn)行合并和匯總。

其實(shí),處理這個問題,要分為兩種場景:同步調(diào)用異步調(diào)用。

8.1 同步調(diào)用

如果在job中需要獲取 2000 個用戶的信息,它要求只要能正確獲取到數(shù)據(jù)就好,對獲取數(shù)據(jù)的總耗時要求不太高。

但對每一次遠(yuǎn)程接口調(diào)用的耗時有要求,不能大于 500ms,不然會有郵件預(yù)警。

這時,我們可以同步分頁調(diào)用批量查詢用戶信息接口。

具體示例代碼如下:

List>allIds=Lists.partition(ids,200);

for(ListbatchIds:allIds){
Listusers=remoteCallUser(batchIds);
}

代碼中用了googleguava工具中的Lists.partition方法,用它來做分頁簡直太好用了,不然要巴拉巴拉寫一大堆分頁的代碼。

8.2 異步調(diào)用

如果是在某個接口中需要獲取 2000 個用戶的信息,它考慮的就需要更多一些。

除了需要考慮遠(yuǎn)程調(diào)用接口的耗時之外,還需要考慮該接口本身的總耗時,也不能超時 500ms。

這時候用上面的同步分頁請求遠(yuǎn)程接口,肯定是行不通的。

那么,只能使用異步調(diào)用了。

代碼如下:

List>allIds=Lists.partition(ids,200);

finalListresult=Lists.newArrayList();
allIds.stream().forEach((batchIds)->{
CompletableFuture.supplyAsync(()->{
result.addAll(remoteCallUser(batchIds));
returnBoolean.TRUE;
},executor);
})

使用 CompletableFuture 類,多個線程異步調(diào)用遠(yuǎn)程接口,最后匯總結(jié)果統(tǒng)一返回。

9.加緩存

解決接口性能問題,加緩存是一個非常高效的方法。

但不能為了緩存而緩存,還是要看具體的業(yè)務(wù)場景。畢竟加了緩存,會導(dǎo)致接口的復(fù)雜度增加,它會帶來數(shù)據(jù)不一致問題。

在有些并發(fā)量比較低的場景中,比如用戶下單,可以不用加緩存。

還有些場景,比如在商城首頁顯示商品分類的地方,假設(shè)這里的分類是調(diào)用接口獲取到的數(shù)據(jù),但頁面暫時沒有做靜態(tài)化。

如果查詢分類樹的接口沒有使用緩存,而直接從數(shù)據(jù)庫查詢數(shù)據(jù),性能會非常差。

那么如何使用緩存呢?

9.1 redis 緩存

通常情況下,我們使用最多的緩存可能是:redismemcached。

但對于 java 應(yīng)用來說,絕大多數(shù)都是使用的 redis,所以接下來我們以 redis 為例。

由于在關(guān)系型數(shù)據(jù)庫,比如:mysql 中,菜單是有上下級關(guān)系的。某個四級分類是某個三級分類的子分類,這個三級分類又是某個二級分類的子分類,而這個二級分類又是某個一級分類的子分類。

這種存儲結(jié)構(gòu)決定了,想一次性查出這個分類樹,并非是一件非常容易的事情。這就需要使用程序遞歸查詢了,如果分類多的話,這個遞歸是比較耗時的。

所以,如果每次都直接從數(shù)據(jù)庫中查詢分類樹的數(shù)據(jù),是一個非常耗時的操作。

這時我們可以使用緩存,大部分情況,接口都直接從緩存中獲取數(shù)據(jù)。操作 redis 可以使用成熟的框架,比如:jedis 和 redisson 等。

用 jedis 偽代碼如下:

Stringjson=jedis.get(key);
if(StringUtils.isNotEmpty(json)){
CategoryTreecategoryTree=JsonUtil.toObject(json);
returncategoryTree;
}
returnqueryCategoryTreeFromDb();

先從 redis 中根據(jù)某個 key 查詢是否有菜單數(shù)據(jù),如果有則轉(zhuǎn)換成對象,直接返回。如果 redis 中沒有查到菜單數(shù)據(jù),則再從數(shù)據(jù)庫中查詢菜單數(shù)據(jù),有則返回。

此外,我們還需要有個 job,每隔一段時間從數(shù)據(jù)庫中查詢菜單數(shù)據(jù),更新到 redis 當(dāng)中,這樣以后每次都能直接從 redis 中獲取菜單的數(shù)據(jù),而無需訪問數(shù)據(jù)庫了。

04d738b8-851d-11ee-939d-92fbcf53809c.png

這樣改造之后,能快速地提升性能。

但這樣做性能提升不是最佳的,還有其他的方案,我們一起看看下面的內(nèi)容。

9.2 二級緩存

上面的方案是基于 redis 緩存的,雖說 redis 訪問速度很快。但畢竟是一個遠(yuǎn)程調(diào)用,而且菜單樹的數(shù)據(jù)很多,在網(wǎng)絡(luò)傳輸?shù)倪^程中,是有些耗時的。

有沒有辦法,不經(jīng)過請求遠(yuǎn)程,就能直接獲取到數(shù)據(jù)呢?

答:使用二級緩存,即基于內(nèi)存的緩存。

除了自己手寫的內(nèi)存緩存之外,目前使用比較多的內(nèi)存緩存框架有:guava、Ehcache、caffine等。

我們這里以caffeine為例,它是 spring 官方推薦的。

第一步,引入 caffeine 的相關(guān) jar 包。

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>com.github.ben-manes.caffeinegroupId>
<artifactId>caffeineartifactId>
<version>2.6.0version>
dependency>

第二步,配置 CacheManager,開啟 EnableCaching。

@Configuration
@EnableCaching
publicclassCacheConfig{
@Bean
publicCacheManagercacheManager(){
CaffeineCacheManagercacheManager=newCaffeineCacheManager();
//Caffeine配置
Caffeinecaffeine=Caffeine.newBuilder()
//最后一次寫入后經(jīng)過固定時間過期
.expireAfterWrite(10,TimeUnit.SECONDS)
//緩存的最大條數(shù)
.maximumSize(1000);
cacheManager.setCaffeine(caffeine);
returncacheManager;
}
}

第三步,使用 Cacheable 注解獲取數(shù)據(jù)。

@Service
publicclassCategoryService{

@Cacheable(value="category",key="#categoryKey")
publicCategoryModelgetCategory(StringcategoryKey){
Stringjson=jedis.get(categoryKey);
if(StringUtils.isNotEmpty(json)){
CategoryTreecategoryTree=JsonUtil.toObject(json);
returncategoryTree;
}
returnqueryCategoryTreeFromDb();
}
}

調(diào)用 categoryService.getCategory() 方法時,先從 caffine 緩存中獲取數(shù)據(jù),如果能夠獲取到數(shù)據(jù),則直接返回該數(shù)據(jù),不進(jìn)入方法體。

如果不能獲取到數(shù)據(jù),則再從 redis 中查一次數(shù)據(jù)。如果查詢到了,則返回數(shù)據(jù),并且放入 caffine 中。

如果還是沒有查到數(shù)據(jù),則直接從數(shù)據(jù)庫中獲取到數(shù)據(jù),然后放到 caffine 緩存中。

具體流程圖如下:

04fd2db6-851d-11ee-939d-92fbcf53809c.png

該方案的性能更好,但有個缺點(diǎn)就是,如果數(shù)據(jù)更新了,不能及時刷新緩存。此外,如果有多臺服務(wù)器節(jié)點(diǎn),可能存在各個節(jié)點(diǎn)上數(shù)據(jù)不一樣的情況。

由此可見,二級緩存給我們帶來性能提升的同時,也帶來了數(shù)據(jù)不一致的問題。使用二級緩存一定要結(jié)合實(shí)際的業(yè)務(wù)場景,并非所有的業(yè)務(wù)場景都適用。

但上面列舉的分類場景,是適合使用二級緩存的。因為它屬于用戶不敏感數(shù)據(jù),即使出現(xiàn)了稍微有點(diǎn)數(shù)據(jù)不一致也沒有關(guān)系,用戶有可能都沒有察覺出來。

10. 分庫分表

有時候,接口性能受限的不是別的,而是數(shù)據(jù)庫。

當(dāng)系統(tǒng)發(fā)展到一定的階段,用戶并發(fā)量大,會有大量的數(shù)據(jù)庫請求,需要占用大量的數(shù)據(jù)庫連接,同時會帶來磁盤 IO 的性能瓶頸問題。

此外,隨著用戶數(shù)量越來越多,產(chǎn)生的數(shù)據(jù)也越來越多,一張表有可能存不下。由于數(shù)據(jù)量太大,sql 語句查詢數(shù)據(jù)時,即使走了索引也會非常耗時。

這時該怎么辦呢?

答:需要做分庫分表。

如下圖所示:

0519ce80-851d-11ee-939d-92fbcf53809c.jpg

圖中將用戶庫拆分成了三個庫,每個庫都包含了四張用戶表。

如果有用戶請求過來的時候,先根據(jù)用戶 id 路由到其中一個用戶庫,然后再定位到某張表。

路由的算法挺多的:

  • 根據(jù) id 取模,比如:id=7,有 4 張表,則 7%4=3,模為 3,路由到用戶表 3。
  • 給 id 指定一個區(qū)間范圍,比如:id 的值是 0-10 萬,則數(shù)據(jù)存在用戶表 0,id 的值是 10-20 萬,則數(shù)據(jù)存在用戶表 1。
  • 一致性 hash 算法

分庫分表主要有兩個方向:垂直水平。

說實(shí)話,垂直方向(即業(yè)務(wù)方向)更簡單。

在水平方向(即數(shù)據(jù)方向)上,分庫和分表的作用,其實(shí)是有區(qū)別的,不能混為一談。

  • 分庫:是為了解決數(shù)據(jù)庫連接資源不足問題和磁盤 IO 的性能瓶頸問題。
  • 分表:是為了解決單表數(shù)據(jù)量太大,sql 語句查詢數(shù)據(jù)時,即使走了索引也非常耗時問題。此外還可以解決消耗 cpu 資源問題。
  • 分庫分表:可以解決數(shù)據(jù)庫連接資源不足、磁盤 IO 的性能瓶頸、檢索數(shù)據(jù)耗時和消耗 cpu 資源等問題。

如果在有些業(yè)務(wù)場景中,用戶并發(fā)量很大,但是需要保存的數(shù)據(jù)量很少,這時可以只分庫,不分表。

如果在有些業(yè)務(wù)場景中,用戶并發(fā)量不大,但是需要保存的數(shù)量很多,這時可以只分表,不分庫。

如果在有些業(yè)務(wù)場景中,用戶并發(fā)量大,并且需要保存的數(shù)量也很多時,可以分庫分表。

11. 輔助功能

優(yōu)化接口性能問題,除了上面提到的這些常用方法之外,還需要配合使用一些輔助功能,因為它們真的可以幫我們提升查找問題的效率。

11.1 開啟慢查詢?nèi)罩?/h3>

通常情況下,為了定位 sql 的性能瓶頸,我們需要開啟 mysql 的慢查詢?nèi)罩?。把超過指定時間的 sql 語句,單獨(dú)記錄下來,方面以后分析和定位問題。

開啟慢查詢?nèi)罩拘枰攸c(diǎn)關(guān)注三個參數(shù):

  • slow_query_log 慢查詢開關(guān)
  • slow_query_log_file 慢查詢?nèi)罩敬娣诺穆窂?/li>
  • long_query_time 超過多少秒才會記錄日志

通過 mysql 的set命令可以設(shè)置:

setglobalslow_query_log='ON';
setglobalslow_query_log_file='/usr/local/mysql/data/slow.log';
setgloballong_query_time=2;

設(shè)置完之后,如果某條 sql 的執(zhí)行時間超過了 2 秒,會被自動記錄到 slow.log 文件中。

當(dāng)然也可以直接修改配置文件my.cnf

[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 2

但這種方式需要重啟 mysql 服務(wù)。

很多公司每天早上都會發(fā)一封慢查詢?nèi)罩镜泥]件,開發(fā)人員根據(jù)這些信息優(yōu)化 sql。

11.2 加監(jiān)控

為了出現(xiàn) sql 問題時,能夠讓我們及時發(fā)現(xiàn),我們需要對系統(tǒng)做監(jiān)控。

目前業(yè)界使用比較多的開源監(jiān)控系統(tǒng)是:Prometheus。

它提供了 監(jiān)控預(yù)警 的功能。

架構(gòu)圖如下:

05490a6a-851d-11ee-939d-92fbcf53809c.png

我們可以用它監(jiān)控如下信息:

  • 接口響應(yīng)時間
  • 調(diào)用第三方服務(wù)耗時
  • 慢查詢 sql 耗時
  • cpu 使用情況
  • 內(nèi)存使用情況
  • 磁盤使用情況
  • 數(shù)據(jù)庫使用情況

等等。。。

它的界面大概長這樣子:

05645ee6-851d-11ee-939d-92fbcf53809c.png

可以看到 mysql 當(dāng)前 qps、活躍線程數(shù)、連接數(shù)、緩存池的大小等信息。

如果發(fā)現(xiàn)數(shù)據(jù)量連接池占用太多,對接口的性能肯定會有影響。

這時可能是代碼中開啟了連接忘了關(guān),或者并發(fā)量太大了導(dǎo)致的,需要做進(jìn)一步排查和系統(tǒng)優(yōu)化。

截圖中只是它一小部分功能,如果你想了解更多功能,可以訪問 Prometheus 的官網(wǎng):https://prometheus.io/。

11.3 鏈路跟蹤

有時候某個接口涉及的邏輯很多,比如:查數(shù)據(jù)庫、查 redis、遠(yuǎn)程調(diào)用接口,發(fā) mq 消息,執(zhí)行業(yè)務(wù)代碼等等。

該接口一次請求的鏈路很長,如果逐一排查,需要花費(fèi)大量的時間,這時候,我們已經(jīng)沒法用傳統(tǒng)的辦法定位問題了。

有沒有辦法解決這問題呢?

用分布式鏈路跟蹤系統(tǒng):skywalking

架構(gòu)圖如下:

058406d8-851d-11ee-939d-92fbcf53809c.png

通過 skywalking 定位性能問題:

059ce2ac-851d-11ee-939d-92fbcf53809c.png

在 skywalking 中可以通過traceId(全局唯一的 id),串聯(lián)一個接口請求的完整鏈路??梢钥吹秸麄€接口的耗時、調(diào)用的遠(yuǎn)程服務(wù)的耗時、訪問數(shù)據(jù)庫或者 redis 的耗時等等,功能非常強(qiáng)大。

之前沒有這個功能的時候,為了定位線上接口性能問題,我們還需要在代碼中加日志,手動打印出鏈路中各個環(huán)節(jié)的耗時情況,然后再逐一排查。


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    8675

    瀏覽量

    151556
  • SQL
    SQL
    +關(guān)注

    關(guān)注

    1

    文章

    772

    瀏覽量

    44193
  • 索引
    +關(guān)注

    關(guān)注

    0

    文章

    59

    瀏覽量

    10485

原文標(biāo)題:接口性能優(yōu)化的11個小技巧,大家務(wù)必掌握!

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    如何優(yōu)化示波器性能?

    本期和大家分享示波器使用的一則技巧——優(yōu)化示波器的性能,由力科技術(shù)專家萬力勱提供。
    發(fā)表于 02-16 09:20 ?1418次閱讀
    如何<b class='flag-5'>優(yōu)化</b>示波器<b class='flag-5'>性能</b>?

    合格的FPGA工程師需要掌握哪些知識?

    合格的FPGA工程師需要掌握哪些知識?這里根據(jù)自己的一些心得總結(jié)一下,其他朋友可以補(bǔ)充啊。 1.Verilog語言及其于硬件電路之間的關(guān)系。 2.器件結(jié)構(gòu)(最好熟練掌握Spartan3
    發(fā)表于 09-12 15:00

    合格的FPGA工程師需要掌握哪些知識?

    合格的FPGA工程師需要掌握哪些知識?一合格的FPGA工程師需要掌握哪些知識?這里根據(jù)自己的一些心得總結(jié)一下,其他朋友可以補(bǔ)充啊。 1.Verilog語言及其于硬件電路之間的關(guān)系
    發(fā)表于 04-21 14:49

    HBase性能優(yōu)化方法總結(jié)

    HBase是Hadoop生態(tài)系統(tǒng)中的一組件,是一分布式、面向列的開源數(shù)據(jù)庫,可以支持?jǐn)?shù)百萬列、超過10億行的數(shù)據(jù)存儲,因此,對HBase性能提出了一定的要求,那么如何進(jìn)行HBase性能
    發(fā)表于 04-20 17:16

    針對于Java的35 代碼性能優(yōu)化總結(jié)

    針對于Java的35 代碼性能優(yōu)化總結(jié) 前言代碼優(yōu)化,一很重要的課題??赡苡行┤擞X得沒用,一些細(xì)小的地方有什么好修改的,改與不改對于代碼
    發(fā)表于 06-19 21:03

    AN0004—AT32 性能優(yōu)化

    本帖最后由 貪玩 于 2022-2-16 21:42 編輯 AN0004—AT32 性能優(yōu)化這篇應(yīng)用筆記描述了如何通過軟件方法提高AT32的運(yùn)行效能。AT32 性能優(yōu)化概述
    發(fā)表于 08-15 14:38

    《現(xiàn)代CPU性能分析與優(yōu)化》--讀書心得筆記

    ;<性能分析與優(yōu)化>>是呼應(yīng)的. 在第一章的導(dǎo)讀里總體介紹了一問題:為什么要做性能優(yōu)化與調(diào)優(yōu). 在生活中我們會發(fā)現(xiàn)一
    發(fā)表于 04-24 15:31

    借助差分接口優(yōu)化射頻收發(fā)器設(shè)計性能

    設(shè)計過程中,工程師需要處理幾個常見問題,包括阻抗匹配、共模電壓匹配以及復(fù)雜的增益計算。了解發(fā)射機(jī)和接收機(jī)中的差分電路對優(yōu)化增益匹配和系統(tǒng)性能很有幫助。 差分接口優(yōu)勢 差分接口有三大主要
    發(fā)表于 11-25 05:55 ?669次閱讀
    借助差分<b class='flag-5'>接口</b><b class='flag-5'>優(yōu)化</b>射頻收發(fā)器設(shè)計<b class='flag-5'>性能</b>

    18種接口優(yōu)化方案匯總1

    之前工作中,遇到一`504`超時問題。原因是因為接口耗時過長,超過`nginx`配置的`10`秒。然后 真槍實(shí)彈搞了一次接口性能優(yōu)化,最后
    的頭像 發(fā)表于 02-15 15:59 ?873次閱讀
    18種<b class='flag-5'>接口</b><b class='flag-5'>優(yōu)化</b>方案匯總1

    18種接口優(yōu)化方案匯總2

    之前工作中,遇到一`504`超時問題。原因是因為接口耗時過長,超過`nginx`配置的`10`秒。然后 真槍實(shí)彈搞了一次接口性能優(yōu)化,最后
    的頭像 發(fā)表于 02-15 15:59 ?698次閱讀
    18種<b class='flag-5'>接口</b><b class='flag-5'>優(yōu)化</b>方案匯總2

    接口優(yōu)化的常見方案實(shí)戰(zhàn)總結(jié)

    針對老項目,去年做了許多降本增效的事情,其中發(fā)現(xiàn)最多的就是接口耗時過長的問題,就集中搞了一次接口性能優(yōu)化。本文將給小伙伴們分享一下接口
    的頭像 發(fā)表于 03-06 09:22 ?589次閱讀

    SpringCloud組件性能優(yōu)化技巧分享

    Springcloud 原始的配置,性能是很低的,大家可以使用Jmeter測試一下,QPS不會到50。要做到高并發(fā),需要做不少的配置優(yōu)化
    的頭像 發(fā)表于 08-16 09:47 ?1304次閱讀
    SpringCloud組件<b class='flag-5'>性能</b><b class='flag-5'>優(yōu)化</b>技巧分享

    性能優(yōu)化之路總結(jié)

    針對老項目,去年做了許多降本增效的事情,其中發(fā)現(xiàn)最多的就是接口耗時過長的問題,就集中搞了一次接口性能優(yōu)化。本文將給小伙伴們分享一下接口
    的頭像 發(fā)表于 06-17 15:00 ?361次閱讀

    如何優(yōu)化SOC芯片性能

    優(yōu)化SOC(System on Chip,系統(tǒng)級芯片)芯片性能是一復(fù)雜而多維的任務(wù),涉及多個方面的優(yōu)化策略。以下是一些關(guān)鍵的優(yōu)化措施: 一
    的頭像 發(fā)表于 10-31 15:50 ?617次閱讀

    RAID 5 性能優(yōu)化技巧

    選擇合適的硬件 高性能硬盤 :選擇具有高轉(zhuǎn)速和高緩存的硬盤可以顯著提升RAID 5的性能。 高速SAS或SATA接口 :使用SAS或SATA 3.0接口可以提供更高的數(shù)據(jù)傳輸速率。
    的頭像 發(fā)表于 12-27 17:05 ?405次閱讀