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

前言

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

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

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

總體來說夭苗,用三招就搞定了。到底經(jīng)歷了什么隔缀?

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

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

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

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

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

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

比如:要查某一個(gè)組織的數(shù)據(jù)执俩,是非承炷疲快的。但如果要查平臺役首,即組織的根節(jié)點(diǎn)尝丐,這種情況下,需要查詢的數(shù)據(jù)量非常大衡奥,接口響應(yīng)就可能會非常慢爹袁。

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

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

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

怎么回事呢盹兢?

當(dāng)初說的需求是這個(gè)接口給分頁的列表頁面調(diào)用邻梆,每頁大小有:10、20绎秒、30浦妄、50、100见芹,用戶可以選擇剂娄。

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

但實(shí)際情況是:結(jié)算單列表頁面還包含了很多訂單“鸭基本上每一個(gè)結(jié)算單故黑,都有多個(gè)訂單。調(diào)用批量評價(jià)查詢接口時(shí)庭砍,需要把結(jié)算單和訂單的數(shù)據(jù)合并到一起场晶。

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

2. 現(xiàn)狀

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

但那個(gè)批量評分查詢接口恨樟,邏輯不簡單。

偽代碼如下:

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)用遠(yuǎn)程接口獲取組織信息
    List<OrgEntity> orgList = feginClient.getOrgByIds(orgIds);
    
    for(SearchEntity entity : list) {
        //通過組織id找組織code
        String orgCode = findOrgCode(orgList, entity.getOrgId());
    
        //通過組合條件查詢評價(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í)場景中疚俱,代碼比這個(gè)復(fù)雜很多劝术,這里為了給大家演示,簡化了一下呆奕。

最關(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價(jià)表中冗余一個(gè)組織code字段绳泉,萬一哪天組織表中的組織code有修改,不得不通過某種機(jī)制姆泻,通知我們同步修改評價(jià)表的組織code圈纺,不然就會出現(xiàn)數(shù)據(jù)不一致的問題秦忿。

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

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

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

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

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

由于業(yè)務(wù)系統(tǒng)調(diào)用這個(gè)接口時(shí)穆碎,沒有傳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)...

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

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

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

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

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

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

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

評價(jià)表之前建立一個(gè)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四個(gè)字段組成蒂培。

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

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

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

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

只在一個(gè)線程中查詢數(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, //線程池中線程的最大空閑時(shí)間,超過這個(gè)時(shí)間空閑線程將被回收
    TimeUnit.SECONDS,//時(shí)間單位
    new ArrayBlockingQueue(500), //隊(duì)列
    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;

    /**
     * 空閑線程存活時(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)過這次優(yōu)化凿傅,接口性能也提升了5倍缠犀。

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

但整體效果還不太理想辨液。

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

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

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

那么被辑,我們?yōu)槭裁床幌拗埔幌铝呛罚看尾樵兊挠涗洍l數(shù)呢?

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

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

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

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

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

5.1 前端做分頁

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末橄镜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冯乘,更是在濱河造成了極大的恐慌洽胶,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裆馒,死亡現(xiàn)場離奇詭異姊氓,居然都是意外死亡丐怯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門翔横,熙熙樓的掌柜王于貴愁眉苦臉地迎上來读跷,“玉大人,你說我怎么就攤上這事禾唁⌒Ю溃” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵荡短,是天一觀的道長丐枉。 經(jīng)常有香客問我,道長肢预,這世上最難降的妖魔是什么矛洞? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮烫映,結(jié)果婚禮上沼本,老公的妹妹穿的比我還像新娘。我一直安慰自己锭沟,他們只是感情好抽兆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著族淮,像睡著了一般辫红。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上祝辣,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天贴妻,我揣著相機(jī)與錄音,去河邊找鬼蝙斜。 笑死名惩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孕荠。 我是一名探鬼主播娩鹉,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼稚伍!你這毒婦竟也來了弯予?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤个曙,失蹤者是張志新(化名)和其女友劉穎锈嫩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呼寸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年那槽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片等舔。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糟趾,靈堂內(nèi)的尸體忽然破棺而出慌植,到底是詐尸還是另有隱情,我是刑警寧澤义郑,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布蝶柿,位于F島的核電站,受9級特大地震影響非驮,放射性物質(zhì)發(fā)生泄漏交汤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一劫笙、第九天 我趴在偏房一處隱蔽的房頂上張望芙扎。 院中可真熱鬧,春花似錦填大、人聲如沸戒洼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽圈浇。三九已至,卻和暖如春靴寂,著一層夾襖步出監(jiān)牢的瞬間磷蜀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工百炬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褐隆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓收壕,卻偏偏與公主長得像妓灌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蜜宪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容