自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從20s優(yōu)化到500ms,我用了這三招

開(kāi)發(fā) 前端
本文將會(huì)接著接口性能優(yōu)化這個(gè)話題,從實(shí)戰(zhàn)的角度出發(fā),聊聊我是如何優(yōu)化一個(gè)慢查詢接口的。

前言

接口性能問(wèn)題,對(duì)于從事后端開(kāi)發(fā)的同學(xué)來(lái)說(shuō),是一個(gè)繞不開(kāi)的話題。想要優(yōu)化一個(gè)接口的性能,需要從多個(gè)方面著手。

其實(shí),我之前也寫(xiě)過(guò)一篇接口性能優(yōu)化相關(guān)的文章《??聊聊接口性能優(yōu)化的11個(gè)小技巧??》,發(fā)表之后在全網(wǎng)廣受好評(píng),感興趣的小伙們可以仔細(xì)看看。

本文將會(huì)接著接口性能優(yōu)化這個(gè)話題,從實(shí)戰(zhàn)的角度出發(fā),聊聊我是如何優(yōu)化一個(gè)慢查詢接口的。

上周我優(yōu)化了一下線上的批量評(píng)分查詢接口,將接口性能從最初的20s?,優(yōu)化到目前的500ms以內(nèi)。

總體來(lái)說(shuō),用三招就搞定了。

到底經(jīng)歷了什么?

1. 案發(fā)現(xiàn)場(chǎng)

我們每天早上上班前,都會(huì)收到一封線上慢查詢接口匯總郵件,郵件中會(huì)展示接口地址、調(diào)用次數(shù)、最大耗時(shí)、平均耗時(shí)和traceId等信息。

我看到其中有一個(gè)批量評(píng)分查詢接口,最大耗時(shí)達(dá)到了20s?,平均耗時(shí)也有2s。

用skywalking查看該接口的調(diào)用信息,發(fā)現(xiàn)絕大數(shù)情況下,該接口響應(yīng)還是比較快的,大部分情況都是500s左右就能返回,但也有少部分超過(guò)了20s的請(qǐng)求。

這個(gè)現(xiàn)象就非常奇怪了。

莫非跟數(shù)據(jù)有關(guān)?

比如:要查某一個(gè)組織的數(shù)據(jù),是非??斓?。但如果要查平臺(tái),即組織的根節(jié)點(diǎn),這種情況下,需要查詢的數(shù)據(jù)量非常大,接口響應(yīng)就可能會(huì)非常慢。

但事實(shí)證明不是這個(gè)原因。

很快有個(gè)同事給出了答案。

他們?cè)诮Y(jié)算單列表頁(yè)面中,批量請(qǐng)求了這個(gè)接口,但他傳參的數(shù)據(jù)量非常大。

怎么回事呢?

當(dāng)初說(shuō)的需求是這個(gè)接口給分頁(yè)的列表頁(yè)面調(diào)用,每頁(yè)大小有:10、20、30、50、100,用戶可以選擇。

換句話說(shuō),調(diào)用批量評(píng)價(jià)查詢接口,一次性最多可以查詢100條記錄。

但實(shí)際情況是:結(jié)算單列表頁(yè)面還包含了很多訂單?;旧厦恳粋€(gè)結(jié)算單,都有多個(gè)訂單。調(diào)用批量評(píng)價(jià)查詢接口時(shí),需要把結(jié)算單和訂單的數(shù)據(jù)合并到一起。

這樣導(dǎo)致的結(jié)果是:調(diào)用批量評(píng)價(jià)查詢接口時(shí),一次性傳入的參數(shù)非常多,入?yún)ist中包含幾百、甚至幾千條數(shù)據(jù)都有可能。

2. 現(xiàn)狀

如果一次性傳入幾百或者幾千個(gè)id,批量查詢數(shù)據(jù)還好,可以走主鍵索引,查詢效率也不至于太差。

但那個(gè)批量評(píng)分查詢接口,邏輯不簡(jiǎn)單。

偽代碼如下:

public List<ScoreEntity> query(List<SearchEntity> list) {
//結(jié)果
List<ScoreEntity> result = Lists.newArrayList();
//獲取組織id
List<Long> orgIds = list.stream().map(SearchEntity::getOrgId).collect(Collectors.toList());
//通過(guò)regin調(diào)用遠(yuǎn)程接口獲取組織信息
List<OrgEntity> orgList = feginClient.getOrgByIds(orgIds);

for(SearchEntity entity : list) {
//通過(guò)組織id找組織code
String orgCode = findOrgCode(orgList, entity.getOrgId());

//通過(guò)組合條件查詢?cè)u(píng)價(jià)
ScoreSearchEntity scoreSearchEntity = new ScoreSearchEntity();
scoreSearchEntity.setOrgCode(orgCode);
scoreSearchEntity.setCategoryId(entity.getCategoryId());
scoreSearchEntity.setBusinessId(entity.getBusinessId());
scoreSearchEntity.setBusinessType(entity.getBusinessType());
List<ScoreEntity> resultList = scoreMapper.queryScore(scoreSearchEntity);

if(CollectionUtils.isNotEmpty(resultList)) {
ScoreEntity scoreEntity = resultList.get(0);
result.add(scoreEntity);
}
}
return result;
}

其實(shí)在真實(shí)場(chǎng)景中,代碼比這個(gè)復(fù)雜很多,這里為了給大家演示,簡(jiǎn)化了一下。

最關(guān)鍵的地方有兩點(diǎn):

  • 在接口中遠(yuǎn)程調(diào)用了另外一個(gè)接口
  • 需要在for循環(huán)中查詢數(shù)據(jù)

其中的第1點(diǎn),即:在接口中遠(yuǎn)程調(diào)用了另外一個(gè)接口,這個(gè)代碼是必須的。

因?yàn)槿绻谠u(píng)價(jià)表?中冗余一個(gè)組織code字段,萬(wàn)一哪天組織表中的組織code有修改,不得不通過(guò)某種機(jī)制,通知我們同步修改評(píng)價(jià)表的組織code,不然就會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。

很顯然,如果要這樣調(diào)整的話,業(yè)務(wù)流程上要改了,代碼改動(dòng)有點(diǎn)大。

所以,還是先保持在接口中遠(yuǎn)程調(diào)用吧。

這樣看來(lái),可以優(yōu)化的地方只能在:for循環(huán)中查詢數(shù)據(jù)。

3. 第一次優(yōu)化

由于需要在for循環(huán)中,每條記錄都要根據(jù)不同的條件,查詢出想要的數(shù)據(jù)。

由于業(yè)務(wù)系統(tǒng)調(diào)用這個(gè)接口時(shí),沒(méi)有傳id?,不好在where?條件中用id in (...),這方式批量查詢數(shù)據(jù)。

其實(shí),有一種辦法不用循環(huán)查詢,一條sql就能搞定需求:使用or?關(guān)鍵字拼接,例如:(org_code='001' and category_id=123 and business_id=111 and business_type=1) or? (org_code='002' and category_id=123 and business_id=112 and business_type=2) or (org_code='003' and category_id=124 and business_id=117 and business_type=1)...

這種方式會(huì)導(dǎo)致sql語(yǔ)句會(huì)非常長(zhǎng),性能也會(huì)很差。

其實(shí)還有一種寫(xiě)法:

where (a,b) in ((1,2),(1,3)...)

不過(guò)這種sql,如果一次性查詢的數(shù)據(jù)量太多的話,性能也不太好。

居然沒(méi)法改成批量查詢,就只能優(yōu)化單條查詢sql的執(zhí)行效率了。

首先從索引入手,因?yàn)楦脑斐杀咀畹汀?/p>

第一次優(yōu)化是優(yōu)化索引。

評(píng)價(jià)表之前建立一個(gè)business_id字段的普通索引,但是從目前來(lái)看效率不太理想。

由于我果斷加了聯(lián)合索引:

alter table user_score add index  `un_org_category_business` (`org_code`,`category_id`,`business_id`,`business_type`) USING BTREE;

該聯(lián)合索引由:org_code、category_id、business_id和business_type四個(gè)字段組成。

經(jīng)過(guò)這次優(yōu)化,效果立竿見(jiàn)影。

批量評(píng)價(jià)查詢接口最大耗時(shí),從最初的20s?,縮短到了5s左右。

4. 第二次優(yōu)化

由于需要在for循環(huán)中,每條記錄都要根據(jù)不同的條件,查詢出想要的數(shù)據(jù)。

只在一個(gè)線程中查詢數(shù)據(jù),顯然太慢。

那么,為何不能改成多線程調(diào)用?

第二次優(yōu)化,查詢數(shù)據(jù)庫(kù)由單線程?改成多線程。

但由于該接口是要將查詢出的所有數(shù)據(jù),都返回回去的,所以要獲取查詢結(jié)果。

使用多線程調(diào)用,并且要獲取返回值,這種場(chǎng)景使用java8中的CompleteFuture非常合適。

代碼調(diào)整為:

CompletableFuture[] futureArray = dataList.stream()
.map(data -> CompletableFuture
.supplyAsync(() -> query(data), asyncExecutor)
.whenComplete((result, th) -> {
})).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futureArray).join();

CompleteFuture?的本質(zhì)是創(chuàng)建線程?執(zhí)行,為了避免產(chǎn)生太多的線程,所以使用線程池是非常有必要的。

優(yōu)先推薦使用ThreadPoolExecutor類(lèi),我們自定義線程池。

具體代碼如下:

ExecutorService threadPool = new ThreadPoolExecutor(
8, //corePoolSize線程池中核心線程數(shù)
10, //maximumPoolSize 線程池中最大線程數(shù)
60, //線程池中線程的最大空閑時(shí)間,超過(guò)這個(gè)時(shí)間空閑線程將被回收
TimeUnit.SECONDS,//時(shí)間單位
new ArrayBlockingQueue(500), //隊(duì)列
new ThreadPoolExecutor.CallerRunsPolicy()); //拒絕策略

也可以使用ThreadPoolTaskExecutor類(lèi)創(chuàng)建線程池:

@Configuration
public class ThreadPoolConfig {

/**
* 核心線程數(shù)量,默認(rèn)1
*/
private int corePoolSize = 8;

/**
* 最大線程數(shù)量,默認(rèn)Integer.MAX_VALUE;
*/
private int maxPoolSize = 10;

/**
* 空閑線程存活時(shí)間
*/
private int keepAliveSeconds = 60;

/**
* 線程阻塞隊(duì)列容量,默認(rèn)Integer.MAX_VALUE
*/
private int queueCapacity = 1;

/**
* 是否允許核心線程超時(shí)
*/
private boolean allowCoreThreadTimeOut = false;


@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
// 設(shè)置拒絕策略,直接在execute方法的調(diào)用線程中運(yùn)行被拒絕的任務(wù)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 執(zhí)行初始化
executor.initialize();
return executor;
}
}

經(jīng)過(guò)這次優(yōu)化,接口性能也提升了5倍。

從5s?左右,縮短到1s左右。

但整體效果還不太理想。

5. 第三次優(yōu)化

經(jīng)過(guò)前面的兩次優(yōu)化,批量查詢?cè)u(píng)價(jià)接口性能有一些提升,但耗時(shí)還是大于1s。

出現(xiàn)這個(gè)問(wèn)題的根本原因是:一次性查詢的數(shù)據(jù)太多。

那么,我們?yōu)槭裁床幌拗埔幌?,每次查詢的記錄條數(shù)呢?

第三次優(yōu)化,限制一次性查詢的記錄條數(shù)。其實(shí)之前也做了限制,不過(guò)最大是2000條記錄,從目前看效果不好。

限制該接口一次只能查200?條記錄,如果超過(guò)200條則會(huì)報(bào)錯(cuò)提示。

如果直接對(duì)該接口做限制,則可能會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)出現(xiàn)異常。

為了避免這種情況的發(fā)生,必須跟業(yè)務(wù)系統(tǒng)團(tuán)隊(duì)一起討論一下優(yōu)化方案。

主要有下面兩個(gè)方案:

5.1 前端做分頁(yè)

在結(jié)算單列表頁(yè)中,每個(gè)結(jié)算單默認(rèn)只展示1個(gè)訂單,多余的分頁(yè)查詢。

這樣的話,如果按照每頁(yè)最大100條記錄計(jì)算的話,結(jié)算單和訂單最多一次只能查詢200條記錄。

這就需要業(yè)務(wù)系統(tǒng)的前端做分頁(yè)功能?,同時(shí)后端接口要調(diào)整支持分頁(yè)查詢。

但目前現(xiàn)狀是前端沒(méi)有多余開(kāi)發(fā)資源。

由于人手不足的原因,這套方案目前只能暫時(shí)擱置。

5.2 分批調(diào)用接口

業(yè)務(wù)系統(tǒng)后端之前是一次性?調(diào)用評(píng)價(jià)查詢接口,現(xiàn)在改成分批調(diào)用。

比如:之前查詢500條記錄,業(yè)務(wù)系統(tǒng)只調(diào)用一次查詢接口。

現(xiàn)在改成業(yè)務(wù)系統(tǒng)每次只查100條記錄,分5批調(diào)用,總共也是查詢500條記錄。

這樣不是變慢了嗎?

答:如果那5批調(diào)用評(píng)價(jià)查詢接口的操作,是在for循環(huán)中單線程順序的,整體耗時(shí)當(dāng)然可能會(huì)變慢。

但業(yè)務(wù)系統(tǒng)也可以改成多線程調(diào)用,只需最終匯總結(jié)果即可。

此時(shí),有人可能會(huì)問(wèn)題:在評(píng)價(jià)查詢接口的服務(wù)器多線程調(diào)用,跟在其他業(yè)務(wù)系統(tǒng)中多線程調(diào)用不是一回事?

還不如把批量評(píng)價(jià)查詢接口的服務(wù)器中,線程池的最大線程數(shù)調(diào)大一點(diǎn)?

顯然你忽略了一件事:線上應(yīng)用一般不會(huì)被部署成單點(diǎn)。絕大多數(shù)情況下,為了避免因?yàn)榉?wù)器掛了,造成單點(diǎn)故障,基本會(huì)部署至少2個(gè)節(jié)點(diǎn)。這樣即使一個(gè)節(jié)點(diǎn)掛了,整個(gè)應(yīng)用也能正常訪問(wèn)。

當(dāng)然也可能會(huì)出現(xiàn)這種情況:假如掛了一個(gè)節(jié)點(diǎn),另外一個(gè)節(jié)點(diǎn)可能因?yàn)樵L問(wèn)的流量太大了,扛不住壓力,也可能因此掛掉。

換句話說(shuō),通過(guò)業(yè)務(wù)系統(tǒng)中的多線程調(diào)用接口,可以將訪問(wèn)接口的流量負(fù)載均衡到不同的節(jié)點(diǎn)上。

他們也用8個(gè)線程,將數(shù)據(jù)分批,每批100條記錄,最后將結(jié)果匯總。

經(jīng)過(guò)這次優(yōu)化,接口性能再次提升了1倍。

從1s?左右,縮短到小于500ms。

溫馨提醒一下,無(wú)論是在批量查詢?cè)u(píng)價(jià)接口查詢數(shù)據(jù)庫(kù),還是在業(yè)務(wù)系統(tǒng)中調(diào)用批量查詢?cè)u(píng)價(jià)接口,使用多線程調(diào)用,都只是一個(gè)臨時(shí)方案,并不完美。

這樣做的原因主要是為了先快速解決問(wèn)題,因?yàn)檫@種方案改動(dòng)是最小的。

要從根本上解決問(wèn)題,需要重新設(shè)計(jì)這一套功能,需要修改表結(jié)構(gòu),甚至可能需要修改業(yè)務(wù)流程。但由于牽涉到多條業(yè)務(wù)線,多個(gè)業(yè)務(wù)系統(tǒng),只能排期慢慢做了。

責(zé)任編輯:武曉燕 來(lái)源: 蘇三說(shuō)技術(shù)
相關(guān)推薦

2022-08-14 14:32:06

接口優(yōu)化

2022-09-19 08:41:02

數(shù)據(jù)查詢分離

2024-05-28 08:47:52

2023-09-27 08:21:00

查詢分離數(shù)據(jù)API

2024-08-30 09:31:36

2024-09-29 08:21:06

2022-09-27 08:40:44

慢查詢MySQL定位優(yōu)化

2023-05-14 17:16:22

分類(lèi)樹(shù)SpringBoot

2021-01-14 16:28:15

蠕蟲(chóng)病毒刪除系統(tǒng)安全專(zhuān)家

2022-07-05 10:50:31

數(shù)據(jù)庫(kù)查詢實(shí)戰(zhàn)

2020-09-01 11:10:39

數(shù)據(jù)庫(kù)鏈接池HikariCP

2018-01-02 10:46:24

微信騰訊表情

2024-03-04 08:29:33

數(shù)據(jù)定制化Java

2024-07-30 14:26:52

2019-06-20 11:20:25

sql優(yōu)化數(shù)據(jù)庫(kù)

2020-02-23 17:15:29

SQL分析查詢

2024-04-17 08:21:44

2023-12-25 08:24:03

雙異步數(shù)據(jù)庫(kù)Excel

2020-03-05 09:42:43

JavaJava虛擬機(jī)數(shù)據(jù)庫(kù)

2025-02-14 09:30:42

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)