日韩国产精品99成人不卡在线无毒|狠狠躁夜夜爽一级二级精品|亚洲日日噜噜孕妇中文字幕|日韩久草中文三级片

新聞動(dòng)態(tài)

接口性能優(yōu)化的那些小技巧

網(wǎng)站優(yōu)化 發(fā)布者:ou3377 2021-12-09 09:42 訪問量:249

前言

接口性能優(yōu)化對(duì)于從事后端開發(fā)的同學(xué)來說,肯定再熟悉不過了,因?yàn)樗且粋€(gè)跟開發(fā)語言無關(guān)的公共問題。

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

有時(shí)候,只需加個(gè)索引就能解決問題。

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

有時(shí)候,需要增加緩存。

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

有時(shí)候,需要需要分庫分表。

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

等等。。。

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

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

1.索引

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

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

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

這時(shí)你可能會(huì)有下面這些疑問:

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

1.1 沒加索引

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

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

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

可以通過命令:

show index from `order`;

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

也可以通過命令:

show create table `order`;

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

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

ALTER TABLE `order` ADD INDEX idx_name (name);

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

CREATE INDEX idx_name ON `order` (name);

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

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

刪除索引可以用DROP INDEX命令:

ALTER TABLE `order` DROP INDEX idx_name;

DROP INDEX命令也行:

DROP INDEX idx_name ON `order`;

1.2 索引沒生效

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

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

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

例如:

explain select * from `order` where code='002';

結(jié)果:圖片通過這幾列可以判斷索引使用情況,執(zhí)行計(jì)劃包含列的含義如下圖所示:圖片如果你想進(jìn)一步了解explain的詳細(xì)用法,可以看看我的另一篇文章《explain | 索引優(yōu)化的這把絕世好劍,你真的會(huì)用嗎?

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

下面說說索引失效的常見原因:圖片如果不是上面的這些原因,則需要再進(jìn)一步排查一下其他原因。

1.3 選錯(cuò)索引

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

沒錯(cuò),有時(shí)候mysql會(huì)選錯(cuò)索引。

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

至于為什么mysql會(huì)選錯(cuò)索引,后面有專門的文章介紹的,這里先留點(diǎn)懸念。

2. sql優(yōu)化

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

接下來試著優(yōu)化一下sql語句,因?yàn)樗母脑斐杀鞠鄬?duì)于java代碼來說也要小得多。

下面給大家列舉了sql優(yōu)化的15個(gè)小技巧:圖片由于這些技巧在我之前的文章中已經(jīng)詳細(xì)介紹過了,在這里我就不深入了。

更詳細(xì)的內(nèi)容,可以看我的另一篇文章《聊聊sql優(yōu)化的15個(gè)小技巧》,相信看完你會(huì)有很多收獲。

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

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

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

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

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

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

調(diào)用過程如下圖所示:圖片調(diào)用遠(yuǎn)程接口總耗時(shí) 530ms = 200ms + 150ms + 180ms

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

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

3.1 并行調(diào)用

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

如下圖所示:圖片調(diào)用遠(yuǎn)程接口總耗時(shí) 200ms = 200ms(即耗時(shí)最長的那次遠(yuǎn)程接口調(diào)用)

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

java8以后通過CompleteFuture類實(shí)現(xiàn)該功能。我們這里以CompleteFuture為例:

public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
    final UserInfo userInfo = new UserInfo();
    CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteUserAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);

    CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteBonusAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);

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

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

    return userInfo;
}

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

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

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

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

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

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

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

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

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

不信,我們一起看看。

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

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

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

public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }

    List<User> result = Lists.newArrayList();
    searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
    return result;
}

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

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

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

具體代碼如下:

public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }
    List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
    return userMapper.getUserByIds(ids);
}

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

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

4.2 死循環(huán)

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

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

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

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

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

當(dāng)滿足condition等于true的時(shí)候,則自動(dòng)退出該循環(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ù),就會(huì)立即出現(xiàn)死循環(huán)。

4.3 無限遞歸

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

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

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

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

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

5. 異步處理

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

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

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

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

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

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

5.1 線程池

使用線程池改造之后,接口邏輯如下:圖片發(fā)站內(nèi)通知和用戶操作日志功能,被提交到了兩個(gè)單獨(dú)的線程池中。

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

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

那么這個(gè)問題該怎么辦呢?

5.2 mq

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

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

6. 避免大事務(wù)

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

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

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

下面用一張圖看看大事務(wù)引發(fā)的問題。圖片從圖中能夠看出,大事務(wù)問題可能會(huì)造成接口超時(shí),對(duì)接口的性能有直接的影響。

我們?cè)撊绾蝺?yōu)化大事務(wù)呢?

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



關(guān)鍵字: 接口性能優(yōu)化 SQL優(yōu)化

文章連接: http://www.hsjyfc.com.cn/wzyh/791.html

版權(quán)聲明:文章由 晨展科技 整理收集,來源于互聯(lián)網(wǎng)或者用戶投稿,如有侵權(quán),請(qǐng)聯(lián)系我們,我們會(huì)立即刪除。如轉(zhuǎn)載請(qǐng)保留