1. 
          

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

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

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

            前言

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

            該問(wèn)題說(shuō)簡(jiǎn)單也簡(jiǎn)單,說(shuō)復雜也復雜。

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

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

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

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

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

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

            等等。。。

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

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

            1.索引

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

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

            你通過(guò)查看線(xiàn)上日志或者監控報告,查到某個(gè)接口用到的某條sql語(yǔ)句耗時(shí)比較長(cháng)。

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

            1. 該sql語(yǔ)句加索引了沒(méi)?
            2. 加的索引生效了沒(méi)?
            3. mysql選錯索引了沒(méi)?

            1.1 沒(méi)加索引

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

            項目剛開(kāi)始的時(shí)候,由于表中的數據量小,加不加索引sql查詢(xún)性能差別不大。

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

            可以通過(guò)命令:

            show index from `order`;

            能單獨查看某張表的索引情況。

            也可以通過(guò)命令:

            show create table `order`;

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

            通過(guò)ALTER TABLE命令可以添加索引:

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

            也可以通過(guò)CREATE INDEX命令添加索引:

            CREATE INDEX idx_name ON `order` (name);

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

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

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

            ALTER TABLE `order` DROP INDEX idx_name;

            DROP INDEX命令也行:

            DROP INDEX idx_name ON `order`;

            1.2 索引沒(méi)生效

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

            那么,如何查看索引有沒(méi)有生效呢?

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

            例如:

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

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

            說(shuō)實(shí)話(huà),sql語(yǔ)句沒(méi)有走索引,排除沒(méi)有建索引之外,最大的可能性是索引失效了。

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

            1.3 選錯索引

            此外,你有沒(méi)有遇到過(guò)這樣一種情況:明明是同一條sql,只有入參不同而已。有的時(shí)候走的索引a,有的時(shí)候卻走的索引b?

            沒(méi)錯,有時(shí)候mysql會(huì )選錯索引。

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

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

            2. sql優(yōu)化

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

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

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

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

            3. 遠程調用

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

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

            在用戶(hù)信息查詢(xún)接口中需要返回:用戶(hù)名稱(chēng)、性別、等級、頭像、積分、成長(cháng)值等信息。

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

            于是,用戶(hù)信息查詢(xún)接口需要調用用戶(hù)查詢(xún)接口、積分查詢(xún)接口 和 成長(cháng)值查詢(xún)接口,然后匯總數據統一返回。

            調用過(guò)程如下圖所示:圖片調用遠程接口總耗時(shí) 530ms = 200ms + 150ms + 180ms

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

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

            3.1 并行調用

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

            如下圖所示:圖片調用遠程接口總耗時(shí) 200ms = 200ms(即耗時(shí)最長(cháng)的那次遠程接口調用)

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

            java8以后通過(guò)CompleteFuture類(lèi)實(shí)現該功能。我們這里以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;
            }

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

            3.2 數據異構

            上面說(shuō)到的用戶(hù)信息查詢(xún)接口需要調用用戶(hù)查詢(xún)接口、積分查詢(xún)接口 和 成長(cháng)值查詢(xún)接口,然后匯總數據統一返回。

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

            如果在高并發(fā)的場(chǎng)景下,為了提升接口性能,遠程接口調用大概率會(huì )被去掉,而改成保存冗余數據的數據異構方案。

            圖片但需要注意的是,如果使用了數據異構方案,就可能會(huì )出現數據一致性問(wèn)題。

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

            4. 重復調用

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

            不信,我們一起看看。

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

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

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

            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è)用戶(hù),則需要循環(huán)50次,去查詢(xún)數據庫。我們都知道,每查詢(xún)一次數據庫,就是一次遠程調用。

            如果查詢(xún)50次數據庫,就有50次遠程調用,這是非常耗時(shí)的操作。

            那么,我們如何優(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è)根據用戶(hù)id集合批量查詢(xún)用戶(hù)的接口,只遠程調用一次,就能查詢(xún)出所有的數據。

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

            4.2 死循環(huán)

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

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

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

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

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

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

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

            出現死循環(huán),大概率是開(kāi)發(fā)人員人為的bug導致的,不過(guò)這種情況很容易被測出來(lái)。

            還有一種隱藏的比較深的死循環(huán),是由于代碼寫(xiě)的不太嚴謹導致的。如果用正常數據,可能測不出問(wèn)題,但一旦出現異常數據,就會(huì )立即出現死循環(huán)。

            4.3 無(wú)限遞歸

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

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

            正常情況下,這段代碼是沒(méi)有問(wèn)題的。

            但如果某次有人誤操作,把某個(gè)分類(lèi)的parentId指向了它自己,這樣就會(huì )出現無(wú)限遞歸的情況。導致接口一直不能返回數據,最終會(huì )發(fā)生堆棧溢出。

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

            5. 異步處理

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

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

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

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

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

            通常異步主要有兩種:多線(xiàn)程 和 mq。

            5.1 線(xiàn)程池

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

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

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

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

            5.2 mq

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

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

            6. 避免大事務(wù)

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

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

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

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

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

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



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

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

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

            双腿国产亚洲精品无码不卡|国产91精品无码麻豆|97久久久久久久极品|无码人妻少妇久久中文字幕
                1.