最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java接口性能從20s優(yōu)化到500ms示例詳解

 更新時間:2022年07月18日 14:33:46   作者:蘇三說技術(shù)  
這篇文章主要為大家介紹了java接口性能從20s優(yōu)化到500ms的操作技巧示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

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

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

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

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

總體來說,用三招就搞定了。

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

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

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

我看到其中有一個批量評分查詢接口,最大耗時達到了20s,平均耗時也有2s。

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

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

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

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

但事實證明不是這個原因。

很快有個同事給出了答案。

他們在結(jié)算單列表頁面中,批量請求了這個接口,但他傳參的數(shù)據(jù)量非常大。

怎么回事呢?

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

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

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

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

2. 現(xiàn)狀

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

但那個批量評分查詢接口,邏輯不簡單。

偽代碼如下:

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());
    //通過regin調(diào)用遠程接口獲取組織信息
    List<OrgEntity> orgList = feginClient.getOrgByIds(orgIds);
    for(SearchEntity entity : list) {
        //通過組織id找組織code
        String orgCode = findOrgCode(orgList, entity.getOrgId());
        //通過組合條件查詢評價
        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;
}

其實在真實場景中,代碼比這個復(fù)雜很多,這里為了給大家演示,簡化了一下。

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

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

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

因為如果在評價表中冗余一個組織code字段,萬一哪天組織表中的組織code有修改,不得不通過某種機制,通知我們同步修改評價表的組織code,不然就會出現(xiàn)數(shù)據(jù)不一致的問題。

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

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

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

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

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

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

其實,有一種辦法不用循環(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)...

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

其實還有一種寫法:

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

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

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

首先從索引入手,因為改造成本最低。

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

評價表之前建立一個business_id字段的普通索引,但是從目前來看效率不太理想。

由于我果斷的加了聯(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四個字段組成。

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

批量評價查詢接口最大耗時,從最初的20s,縮短到了5s左右。

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

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

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

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

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

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

使用多線程調(diào)用,并且要獲取返回值,這種場景使用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類,我們自定義線程池。

具體代碼如下:

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

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

@Configuration
public class ThreadPoolConfig {
    /**
     * 核心線程數(shù)量,默認(rèn)1
     */
    private int corePoolSize = 8;
    /**
     * 最大線程數(shù)量,默認(rèn)Integer.MAX_VALUE;
     */
    private int maxPoolSize = 10;
    /**
     * 空閑線程存活時間
     */
    private int keepAliveSeconds = 60;
    /**
     * 線程阻塞隊列容量,默認(rèn)Integer.MAX_VALUE
     */
    private int queueCapacity = 1;
    /**
     * 是否允許核心線程超時
     */
    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)用線程中運行被拒絕的任務(wù)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 執(zhí)行初始化
        executor.initialize();
        return executor;
    }
}

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

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

但整體效果還不太理想。

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

經(jīng)過前面的兩次優(yōu)化,批量查詢評價接口性能有一些提升,但耗時還是大于1s。

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

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

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

限制該接口一次只能查200條記錄,如果超過200條則會報錯提示。

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

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

主要有下面兩個方案:

5.1 前端做分頁

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

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

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

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

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

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

業(yè)務(wù)系統(tǒng)后端之前是一次性調(diào)用評價查詢接口,現(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)用評價查詢接口的操作,是在for循環(huán)中單線程順序的,整體耗時當(dāng)然可能會變慢。

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

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

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

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

當(dāng)然也可能會出現(xiàn)這種情況:假如掛了一個節(jié)點,另外一個節(jié)點可能因為訪問的流量太大了,扛不住壓力,也可能因此掛掉。

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

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

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

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

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

這樣做的原因主要是為了先快速解決問題,因為這種方案改動是最小的。

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

以上就是java接口性能從20s優(yōu)化到500ms示例詳解的詳細內(nèi)容,更多關(guān)于java接口性能優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java迭代器與Collection接口超詳細講解

    Java迭代器與Collection接口超詳細講解

    Collection也稱集合,集合概述:集合是Java中提供的一種容器,可以用來存儲多個數(shù)據(jù)。Iterator(迭代器)不是一個集合,它是一種用于訪問集合的方法,可用于迭代 ArrayList 和 HashSet 等集合
    2022-07-07
  • 詳解java 客戶端鏈接不上redis解決方案

    詳解java 客戶端鏈接不上redis解決方案

    這篇文章主要介紹了詳解java 客戶端鏈接不上redis解決方案,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-01-01
  • java數(shù)據(jù)結(jié)構(gòu)與算法之冒泡排序詳解

    java數(shù)據(jù)結(jié)構(gòu)與算法之冒泡排序詳解

    這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之冒泡排序,結(jié)合實例形式詳細分析了java冒泡排序的原理、實現(xiàn)技巧與相關(guān)注意事項,需要的朋友可以參考下
    2017-05-05
  • Spring的懶加載機制原理和配置詳解

    Spring的懶加載機制原理和配置詳解

    這篇文章主要介紹了Spring的懶加載機制原理和配置詳解,Spring提供了懶加載機制,所謂的懶加載機制就是可以規(guī)定指定的bean不在啟動時立即創(chuàng)建,而是在后續(xù)第一次用到時才創(chuàng)建,從而減輕在啟動過程中對時間和內(nèi)存的消耗,需要的朋友可以參考下
    2023-10-10
  • java中Map如何根據(jù)key的大小進行排序詳解

    java中Map如何根據(jù)key的大小進行排序詳解

    這篇文章主要給大家介紹了關(guān)于java中Map如何根據(jù)key的大小進行排序的相關(guān)資料,有時候我們業(yè)務(wù)上需要對map里面的值按照key的大小來進行排序的時候我們就可以利用如下方法來進行排序了,需要的朋友可以參考下
    2023-09-09
  • 淺談IDEA2018打包可執(zhí)行jar包的流程

    淺談IDEA2018打包可執(zhí)行jar包的流程

    這篇文章主要介紹了淺談IDEA2018打包可執(zhí)行jar包的流程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • Netty分布式pipeline管道傳播outBound事件源碼解析

    Netty分布式pipeline管道傳播outBound事件源碼解析

    這篇文章主要介紹了Netty分布式pipeline管道傳播outBound事件源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-03-03
  • Java規(guī)則引擎Easy Rules的使用介紹

    Java規(guī)則引擎Easy Rules的使用介紹

    這篇文章主要介紹了Java規(guī)則引擎Easy Rules的使用介紹,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • SpringBoot Bean花式注解方法示例上篇

    SpringBoot Bean花式注解方法示例上篇

    這篇文章主要介紹了SpringBoot Bean花式注解方法,很多時候我們需要根據(jù)不同的條件在容器中加載不同的Bean,或者根據(jù)不同的條件來選擇是否在容器中加載某個Bean
    2023-02-02
  • SpringBoot 整合線程池的示例詳解

    SpringBoot 整合線程池的示例詳解

    線程池是一種利用池化技術(shù)思想來實現(xiàn)的線程管理技術(shù),主要是為了復(fù)用線程、便利地管理線程和任務(wù)、并將線程的創(chuàng)建和任務(wù)的執(zhí)行解耦開來,這篇文章主要介紹了SpringBoot 整合線程池的示例詳解,需要的朋友可以參考下
    2024-08-08

最新評論

梁平县| 屏边| 永善县| 福海县| 永川市| 耿马| 烟台市| 噶尔县| 张掖市| 南京市| 方城县| 房山区| 赤城县| 江华| 辉南县| 东城区| 景东| 永善县| 中牟县| 津市市| 大庆市| 缙云县| 乌苏市| 光山县| 财经| 壶关县| 边坝县| 娄底市| 河南省| 乐亭县| 灌阳县| 松滋市| 皮山县| 晋宁县| 阿拉善盟| 鸡泽县| 荆州市| 类乌齐县| 日照市| 社会| 简阳市|