天天看點

Java後端服務接口性能優化建議

作者:程式猿凱撒

概述

要想成為一名優秀的後端程式員,編寫出高性能的服務接口是一個重要名額,高标準程式員都是對性能反複壓榨的。以下梳理了一些提升接口性能的技術方案,希望對大家有所幫助。

Java後端服務接口性能優化建議

1、資料庫索引

當接口出現性能問題時,最容易想到的就是添加索引,索引優化是代價最小的優化,而且效果很明顯。索引優化主要從一下幾個角度考慮:

SQL是否添加索引? 索引是否生效? 索引設計是否合理?

1.1 SQL是否添加索引

在開發階段就要考慮資料庫表的索引設計,對于一些經常作為檢索條件、order by、group by 後面的字段,且資料區分度高的字段,可以考慮建立索引。

開發階段就把建立索引腳本寫好放在上線待執行腳本檔案中,避免上線時忘記索引建立。

可以通過explain執行查詢計劃,檢視SQL執行情況:

sql複制代碼explain select * from user where name like '%張';
           

也可以通過指令show create table user,檢查整張表的索引情況。

sql複制代碼show create table user
           

如果某個表忘記添加某個索引,可以通過alter table add index指令添加索引。

sql複制代碼alter table user add index idx_name (name);
           

注意:在資料量很大的表中建立索引,最好選擇在業務不繁忙時間段,避免影響線上業務。

1.2 索引不生效

有時候雖然添加了索引,但是索引可能會失效,如下圖梳理了索引失效的14種情況:

Java後端服務接口性能優化建議

(www.bilibili.com/video/BV1yN…

1.3 索引設計是否合理

索引不是設計越多越好,設計必需要合理,例如:

優先考慮設計聯合索引,适當使用覆寫索引; 索引個數盡量不要超過5個; 索引最好選擇資料區分度較高的字段,如性别太多重複字段就不适合建立索引;

2、慢SQL優化

在索引優化之後,還可以進一步優化慢SQL語句,如下梳理10條慢sql優化建議:

Java後端服務接口性能優化建議

3、異步執行

對于一些耗時操作或者不影響主要業務的邏輯,可以采用異步執行,來提升性能。

開始

使用者注冊

短信發送

日志寫入

積分贈送

結束

為了降低接口耗時,及時傳回結果,可以把短信發送、日志寫入及積分贈送通過異步執行。

類似的場景還有:使用者下訂單之後的消息發送及贈送積分也可以放到異步處理。

常見的異步實作:線程池、消息隊列MQ、Spring注解@Async、異步架構CompletableFuture、Spring ApplicationEvent事件。

4、批量調用

資料庫操作或或者遠端調用時,能批量操作就不要for循環調用。

  • 一個簡單例子,我們平時一個清單明細資料插入資料庫時,不要在for循環一條一條插入,建議一個批次幾百條,進行批量插入,減少多次IO,建議使用Mybatis 的foreach操作,不過數量也不要一次太多(100),MP的saveBatch、或者PreparedStatement 的addBatch();
  • 同理遠端調用類似,比如你查詢營銷标簽是否命中,可以一個标簽一個标簽去查,也可以批量标簽去查,那批量進行,效率就更高。
java複制代碼//反例
for(int i=0;i<n;i++){
  singleUpdate(param)
}
//正例
batchUpdate(param);
           

打個比喻:

複制代碼假如你需要搬一萬塊磚到一個地方,你有一輛汽車,汽車一次可以放适量的磚(最多放800), 你可以選擇一次運送一塊磚,也可以一次運送800,你覺得哪種方式更友善,時間消耗更少?
           

5、資料預加載

資料預加載政策,顧名思義就是提前把部分要用到的資料,初始化到緩存。如果你在未來某個時間需要用到某個經過複雜計算的資料,才實時去計算的話,可能耗時比較大。這時候,我們可以采取預取思想,提前把可能需要的資料計算好,放到緩存中,等需要的時候,去緩存取就行。這将大幅度提高接口性能。

場景舉例:

  • 例如地區資料或者一些資料字典資料,可以在項目啟動時預加載到緩存中,在使用時從緩存擷取,提升性能;
  • 部分報表類資料,關聯業務表很多,實時計算比較耗時,可以通過定時任務,在晚上業務不繁忙時,将資料生成好存放到ElasticSearch中,從Es中查詢,提供性能。

項目啟動執行方法:

  • 可以通過實作ApplicationRunner接口中的run方法,實作啟動時執行。方法執行時,項目已經初始化完畢,是可以正常提供服務
java複制代碼public class DataInitUtil implements ApplicationRunner{
  @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("在項目啟動時,會執行這個方法中的代碼");
    }
}
           
  • 使用注解@PostConstruct,需要在項目執行之前執行一些方法,就在目标方法上添加該注解, 存在問題:若執行方法耗時過長,會導緻項目在方法執行期間無法提供服務。
  • 實作CommandLineRunner接口 然後在run方法裡面調用需要調用的方法即可, 可以通過java -jar demo.jar arg1傳參;
  • 實作ApplicationListener接口
typescript複制代碼@Component
public class ApplicationListenerImpl implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("listener");
    }
}
           

**總結:**注解方式@PostConstruct 始終最先執行

  1. 如果監聽的是ApplicationStartedEvent 事件,則一定會在CommandLineRunner和ApplicationRunner 之前執行;
  2. 如果監聽的是ApplicationReadyEvent 事件,則一定會在CommandLineRunner和ApplicationRunner 之後執行;
  3. CommandLineRunner和ApplicationRunner 預設是ApplicationRunner先執行,如果雙方指定了@Order 則按照@Order的大小順序執行,小的先執行。

6、使用緩存技術

在适當的業務場景,恰當地使用緩存,是可以大大提高接口性能的。緩存其實就是一種空間換時間的思想,就是你把要查的資料,提前放好到緩存裡面,需要時,直接查緩存,而避免去查資料庫或者計算的過程。

這裡的緩存包括:Redis緩存,JVM本地緩存,memcached,或者Map等等。我舉個我工作中,一次使用緩存優化的設計吧,比較簡單,但是思路很有借鑒的意義。 場景舉例:

開始

根據使用者賬号

查詢基本資訊

進行首頁展示

結束

這裡使用者的基本資訊包含積分、期望工作地等很多資訊,首頁需要根據這些資訊,個性化展示資料;如果每次都去查詢,比較費時,可以考慮将資料緩存到redis,提高性能。

7、池化技術

池化技術最常見的是線程池應用:

  • 如果你每次需要用到線程,都去建立,就會有增加一定的耗時;
  • 線程池可以重複利用線程,避免不必要的耗時;
  • 池化技術不僅僅指線程池,很多場景都有池化思想的展現,它的本質就是預配置設定與循環使用。

8、事件回調

  • 如果你調用一個系統B的接口,但是它處理業務邏輯,耗時需要10s甚至更多。然後你是一直阻塞等待,直到系統B的下遊接口傳回,再繼續你的下一步操作嗎?這樣顯然不合理。
  • 我們可以采用事件回調機制,即我們不用阻塞等待系統B的接口,而是先去做别的操作。等系統B的接口處理完,通過事件回調通知,我們接口收到通知再進行對應的業務操作即可。如IO多路複用模型實作。

9、串行改為并行調用

假設我們設計一個APP首頁的接口,它需要查使用者獲獎經曆、需要查資格證書、需要查崗位資訊等等。那你是一個一個接口串行調,還是并行調用呢?

可以使用CompletableFuture 并行調用提高性能,類似也可以使用多線程處理。

perl複制代碼// 查詢獲獎經曆
LambdaQueryWrapper<RewardExp> rewardExpQuery = new LambdaQueryWrapper<RewardExp>()
                .eq(RewardExp::getResumeId, resume.getId())
                .eq(RewardExp::getDelFlag, NORMAL)
                .orderByDesc(RewardExp::getDate);
CompletableFuture<List<RewardExp>> rewardExpFuture = CompletableFuture.supplyAsync(() ->
                rewardExpMapper.selectList(rewardExpQuery)
        );

// 查詢資格證書
LambdaQueryWrapper<Credential> credentialQuery = new LambdaQueryWrapper<Credential>()
                .eq(Credential::getResumeId, resume.getId())
                .eq(Credential::getDelFlag, NORMAL)
                .orderByDesc(Credential::getDate);
CompletableFuture<List<Credential>> credentialFuture = CompletableFuture.supplyAsync(() ->
                credentialMapper.selectList(credentialQuery)
        );

// 查詢崗位資訊
LambdaQueryWrapper<ResumeJobs> jobsQuery = new LambdaQueryWrapper<ResumeJobs>()
                .eq(ResumeJobs::getResumeId, resume.getId()).eq(ResumeJobs::getDelFlag, NORMAL);
        CompletableFuture<List<ResumeJobs>> jobsFuture = CompletableFuture.supplyAsync(() ->
                resumeJobsMapper.selectList(jobsQuery)
        );
CompletableFuture.allOf(rewardExpFuture, credentialFuture, jobsFuture).join();
           

10、鎖的粒度和範圍控制

在高并發場景,為了防止超賣等情況,我們經常需要加鎖來保護共享資源。但是,如果加鎖的粒度過粗,是很影響接口性能的。 什麼是加鎖粒度呢?舉一個生活中的示例: 例如:你在家裡上衛生間,你隻需要把衛生間鎖住,而不用把家裡的每個房間都鎖住。 無論是使用synchronized加鎖還是redis分布式鎖,隻需要在共享臨界資源加鎖即可,不涉及共享資源的,就不必要加鎖。 如下示例代碼,隻是方法A涉及共享資源操作,就隻需要鎖住A方法就好; 很多小夥伴容易一鍋端,全鎖住,如果鎖住的代碼耗時,其實是比較影響性能的。 錯誤:

scss複制代碼void wrongMethod(){
    synchronized (this) {
       doMethodB();
       A();
    }
}
           

正确:

scss複制代碼void rightMethod(){
    doMethodB();
    synchronized (this) {
       A();
    }
}
           

11、部分資料暫存檔案中

如果接口耗時瓶頸就在資料庫插入操作這裡,用批量操作等政策,效果還不理想,就可以考慮用檔案或者消息隊列、redis等暫存。有時候批量資料放到檔案,會比插入資料庫效率更高。 該政策的主要思想:就是在大資料量時,将業務資料寫入檔案中,再通過異步的方式去消費檔案中的資料,執行對應的業務邏輯,減少資料庫DB的瞬時壓力。

場景舉例:

開始

調用三方接口

擷取季度考勤明細

資料彙總

資料存儲

結束

由于季度考勤資料量很大,逐個接收處理彙總很費時,可以采用将資料按15天分組寫入到一個檔案中,然後異步去消費檔案中資料進行彙總處理。

12、避免長事務問題

  • 長事務在DB服務端的表現是session持續時間長;
  • 期間可能伴随cpu、記憶體升高,嚴重者可導緻DB服務端整體響應緩慢,導緻線上應用無法使用;
  • 是以線上高并發業務中應該盡量避免長事務的發生。産生長事務的原因,除了sql本身可能存在問題外,和應用層的事務控制邏輯也有很大的關系。

如何避免長事務問題:

  1. RPC遠端調用不要放到事務裡面;
  2. 一些查詢相關的操作,盡量放到事務之外;
  3. 事務中避免處理太多資料;
  4. 并發場景下,盡量避免使用@Transactional注解聲明式事務粒度太大,使用TransactionTemplate的程式設計式事務靈活控制事務的範圍。

如何解決長事務問題:

  1. 增加對長事務的監控,記錄長事務的logId,根據logId能查詢到整個請求調用鍊日志,可以明确是哪個服務的哪個接口的哪個方法産生的;
  2. 根據日志檢查是否存在慢SQL;
  3. 檢查對應服務是否存在RPC遠端調用包裹在事務中;
  4. 檢查是否接口中使用了@Transactional注解聲明式事務。 程式示例:
scss複制代碼@Transactional
public int createUser(User user){
    //儲存使用者資訊
    userDao.save(user);
    passCertDao.updateFlag(user.getPassId());
    // 該方法為遠端RPC接口
    sendEmailRpc(user.getEmail());
    return user.getUserId();
}
           

直接使用@Transactional 注解,Spring的聲明式事務,整個方法都在事務中,而且裡面存在遠端RPC調用,容易出現長事務問題。

13、深度分頁問題

資料庫的深度分頁問題,比較影響接口性能,如下所示SQL語句:

bash複制代碼select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
           

limit 100000,10意味着會掃描100010行,丢棄掉前100000行,最後傳回10行。即使create_time,也會回表很多次。 可以通過标簽記錄法和延遲關聯法來優化深分頁問題。 1. 标簽記錄法 就是标記一下上次查詢到哪一條了,下次再來查的時候,從該條開始往下掃描。就好像看書一樣,上次看到哪裡了,你就折疊一下或者夾個書簽,下次來看的時候,直接就翻到啦。

bash複制代碼select id,name,balance FROM account where id > 100000 limit 10;
           

這樣的話,後面無論翻多少頁,性能都會不錯的,因為命中了id主鍵索引。但是這種方式有局限性:需要一種類似連續自增的字段,而且需要前端把上次最大值傳給後端。

2. 延遲關聯法 延遲關聯法,就是把條件轉移到主鍵索引樹,然後減少回表。優化後的SQL如下:

sql複制代碼select  acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
           

優化思路就是,先通過idx_create_time二級索引樹查詢到滿足條件的主鍵ID,再與原表通過主鍵ID内連接配接,這樣後面直接走了主鍵索引了,同時也減少了回表。

14、程式邏輯優化

優化程式邏輯、程式代碼,是可以節省耗時的。比如,你的程式建立多不必要的對象、或者程式邏輯混亂,多次重複查資料庫、又或者你的實作邏輯算法不是最高效的等等。 常見思路:通過使用visiol模組化工具或者其他方法,梳理清楚代碼邏輯,檢查是否存在不必要的對象建立、邏輯調用或者代碼細節之類的,是否符合一些編碼規範。

15、壓縮傳輸内容

因為檔案太大傳入會比較消耗網絡帶寬資源,進一步影響到服務接口的耗時消耗。例如後端需要傳回圖檔到前端加載,該優化政策可以找UI在不影響圖檔清晰度的前提下,對圖檔進行适當壓縮,節省網絡帶寬,提高接口性能。

16、考慮分庫分表及Nosql

如果确實是因為資料量太大導緻的性能問題,可以考慮使用分庫分表政策,包括垂直拆分和水準拆分,在程式查詢sql中性能得到提升。 同時也可以考慮ElasticSearch,對大資料查詢都有很好的性能支援。 場景舉例: 之前有個業務需求,需要查詢使用者報表資料,報表資料包括使用者次元的很多屬性資訊,使用者表資料量很大,查詢時需要join很多表,如果用關系型資料庫存在嚴重性能問題,如下圖所示

開始

查詢使用者表

查詢積分表

查詢訂單

查詢發票

查詢實名資訊等

結束

**解決方案:**通過XxlJob定時任務,在晚上業務不繁忙時,将使用者報表資料離線生成好,并存放到ElasticSearch中,界面上展示時後端直接通過Es查詢,提高了查詢接口性能。

17、線程池設計

我們使用線程池,就是讓任務并行處理,更高效地完成任務。但是有時候,如果線程池設計不合理,接口執行效率則不太理想。

一般我們需要關注線程池的這幾個參數:核心線程、最大線程數量、阻塞隊列。

  • 如果核心線程過小,則達不到很好的并行效果;
  • 如果阻塞隊列不合理,不僅僅是阻塞的問題,甚至可能會OOM;
  • 如果線程池不區分業務隔離,有可能核心業務被邊緣業務拖垮,如下圖:如果不做線程池隔離,在普通接口出問題時,就會影響核心業務接口,出現生産事故。

普通接口

消息推送

寫入日志

核心接口

首頁加載

登入接口

線程池隔離

18、其他問題

  1. 在抽獎活動或者投票活動等高并發場景下,瞬間出現很大流量的場景,會導緻線程打滿或者DB壓力,是以對于高并發場景下,接口一定要接入限流設定,如Guava限流等;
  2. 有時候操作檔案流或者其他資源,用完之後記得關閉,如果忘記關閉也會導緻資源占用過高,影響性能。

總結

解決服務接口性能問題,是程式員進階的必經之路。 總的來說性能優化通用方法是:從使用者發起請求的整個鍊路分析,将分隔相關環節加上log日志,列印環節耗時,找到接口性能問題出現位置,再結合以上介紹的優化方案進行處理。

作者:美麗的程式人生

連結:https://juejin.cn/post/7239908637094821949

繼續閱讀