<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊接口優(yōu)化的幾種方法

          共 8466字,需瀏覽 17分鐘

           ·

          2022-08-02 01:00

          關(guān)注我們,設(shè)為星標,每天7:40不見不散,架構(gòu)路上與您共享

          回復(fù)架構(gòu)師獲取資源


          大家好,我是你們的朋友架構(gòu)君,一個會寫代碼吟詩的架構(gòu)師。

          'javajgs.com';


          juejin.cn/post/7043423820543164453

          背景

          我負責(zé)的系統(tǒng)到2021年初完成了功能上的建設(shè),開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對性能的吐槽。剛剛收到吐槽的時候,我們的心情是這樣的:
          我有點不信
          當(dāng)越來越多對性能的吐槽反饋到我們這里的時候,我們意識到,接口性能的問題的優(yōu)先級必須提高了。然后我們就跟蹤了1周的接口性能監(jiān)控,這個時候我們的心情是這樣的:
          自己體會這種心情
          有20多個慢接口,5個接口響應(yīng)時間超過5s,1個超過10s,其余的都在2s以上,穩(wěn)定性不足99.8%。作為一個優(yōu)秀的后端程序員,這個數(shù)據(jù)肯定是不能忍的,我們馬上就進入了漫長的接口優(yōu)化之路。本文就是對我們漫長工作歷程的一個總結(jié)。
          正文開始!

          哪些問題會引起接口性能問題?

          這個問題的答案非常多,需要根據(jù)自己的業(yè)務(wù)場景具體分析。這里做一個不完全的總結(jié):
          • 數(shù)據(jù)庫慢查詢
            • 深度分頁問題
            • 未加索引
            • 索引失效
            • join過多
            • 子查詢過多
            • in中的值太多
            • 單純的數(shù)據(jù)量過大
          • 業(yè)務(wù)邏輯復(fù)雜
            • 循環(huán)調(diào)用
            • 順序調(diào)用
          • 線程池設(shè)計不合理
          • 鎖設(shè)計不合理
          • 機器問題(fullGC,機器重啟,線程打滿)

          問題解決

          本文列進的慢查詢問題默認都是基于 MySQL。

          慢查詢(基于mysql)深度分頁

          所謂的深度分頁問題,涉及到mysql分頁的原理。通常情況下,mysql的分頁是這樣寫的:
          select name,code from student limit 100,20
          含義當(dāng)然就是從student表里查100到120這20條數(shù)據(jù),mysql會把前120條數(shù)據(jù)都查出來,拋棄前100條,返回20條。當(dāng)分頁所以深度不大的時候當(dāng)然沒問題,隨著分頁的深入,sql可能會變成這樣:
          select name,code from student limit 1000000,20
          這個時候,mysql會查出來1000020條數(shù)據(jù),拋棄1000000條,如此大的數(shù)據(jù)量,速度一定快不起來。那如何解決呢?一般情況下,最好的方式是增加一個條件:
          select name,code from student where id>1000000  limit 20
          這樣,mysql會走主鍵索引,直接連接到1000000處,然后查出來20條數(shù)據(jù)。但是這個方式需要接口的調(diào)用方配合改造,把上次查詢出來的最大id以參數(shù)的方式傳給接口提供方,會有溝通成本(調(diào)用方:老子不改?。?。

          慢查詢未加索引

          這個是最容易解決的問題,我們可以通過
          show create table xxxx(表名)
          查看某張表的索引。具體加索引的語句網(wǎng)上太多了,不再贅述。不過順便提一嘴,加索引之前,需要考慮一下這個索引是不是有必要加,如果加索引的字段區(qū)分度非常低,那即使加了索引也不會生效。另外,加索引的alter操作,可能引起鎖表,執(zhí)行sql的時候一定要在低峰期(血淚史?。。。。?/section>

          慢查詢索引失效

          這個是慢查詢最不好分析的情況,雖然mysql提供了explain來評估某個sql的查詢性能,其中就有使用的索引。但是為啥索引會失效呢?mysql卻不會告訴咱,需要咱自己分析。大體上,可能引起索引失效的原因有這幾個(可能不完全):
          慢查詢索引失效
          需要特別提出的是,關(guān)于字段區(qū)分性很差的情況,在加索引的時候就應(yīng)該進行評估。如果區(qū)分性很差,這個索引根本就沒必要加。區(qū)分性很差是什么意思呢,舉幾個例子,比如:
          • 某個字段只可能有3個值,那這個字段的索引區(qū)分度就很低。
          • 再比如,某個字段大量為空,只有少量有值;
          • 再比如,某個字段值非常集中,90%都是1,剩下10%可能是2,3,4....
          進一步的,那如果不符合上面所有的索引失效的情況,但是mysql還是不使用對應(yīng)的索引,是為啥呢?這個跟mysql的sql優(yōu)化有關(guān),mysql會在sql優(yōu)化的時候自己選擇合適的索引,很可能是mysql自己的選擇算法算出來使用這個索引不會提升性能,所以就放棄了。這種情況,可以使用force index 關(guān)鍵字強制使用索引(建議修改前先實驗一下,是不是真的會提升查詢效率):
          select name,code from student force index(XXXXXX) where name = '天才' 
          其中xxxx是索引名。

          join過多 or 子查詢過多

          我把join過多 和子查詢過多放在一起說了。一般來說,不建議使用子查詢,可以把子查詢改成join來優(yōu)化。同時,join關(guān)聯(lián)的表也不宜過多,一般來說2-3張表還是合適的。具體關(guān)聯(lián)幾張表比較安全是需要具體問題具體分析的,如果各個表的數(shù)據(jù)量都很少,幾百條幾千條,那么關(guān)聯(lián)的表的可以適當(dāng)多一些,反之則需要少一些。
          另外需要提到的是,在大多數(shù)情況下join是在內(nèi)存里做的,如果匹配的量比較小,或者join_buffer設(shè)置的比較大,速度也不會很慢。但是,當(dāng)join的數(shù)據(jù)量比較大的時候,mysql會采用在硬盤上創(chuàng)建臨時表的方式進行多張表的關(guān)聯(lián)匹配,這種顯然效率就極低,本來磁盤的IO就不快,還要關(guān)聯(lián)。
          一般遇到這種情況的時候就建議從代碼層面進行拆分,在業(yè)務(wù)層先查詢一張表的數(shù)據(jù),然后以關(guān)聯(lián)字段作為條件查詢關(guān)聯(lián)表形成map,然后在業(yè)務(wù)層進行數(shù)據(jù)的拼裝。一般來說,索引建立正確的話,會比join快很多,畢竟內(nèi)存里拼接數(shù)據(jù)要比網(wǎng)絡(luò)傳輸和硬盤IO快得多。

          in的元素過多

          這種問題,如果只看代碼的話不太容易排查,最好結(jié)合監(jiān)控和數(shù)據(jù)庫日志一起分析。如果一個查詢有in,in的條件加了合適的索引,這個時候的sql還是比較慢就可以高度懷疑是in的元素過多。一旦排查出來是這個問題,解決起來也比較容易,不過是把元素分個組,每組查一次。想再快的話,可以再引入多線程。
          進一步的,如果in的元素量大到一定程度還是快不起來,這種最好還是有個限制
          select id from student where id in (1,2,3 ...... 1000limit 200
          當(dāng)然了,最好是在代碼層面做個限制
          if (ids.size() > 200) {
              throw new Exception("單次查詢數(shù)據(jù)量不能超過200");
          }

          單純的數(shù)據(jù)量過大

          這種問題,單純代碼的修修補補一般就解決不了了,需要變動整個的數(shù)據(jù)存儲架構(gòu)?;蛘呤菍Φ讓觤ysql分表或分庫+分表;或者就是直接變更底層數(shù)據(jù)庫,把mysql轉(zhuǎn)換成專門為處理大數(shù)據(jù)設(shè)計的數(shù)據(jù)庫。這種工作是個系統(tǒng)工程,需要嚴密的調(diào)研、方案設(shè)計、方案評審、性能評估、開發(fā)、測試、聯(lián)調(diào),同時需要設(shè)計嚴密的數(shù)據(jù)遷移方案、回滾方案、降級措施、故障處理預(yù)案。除了以上團隊內(nèi)部的工作,還可能有跨系統(tǒng)溝通的工作,畢竟做了重大變更,下游系統(tǒng)的調(diào)用接口的方式有可能會需要變化。
          出于篇幅的考慮,這個不再展開了,筆者有幸完整參與了一次億級別數(shù)據(jù)量的數(shù)據(jù)庫分表工作,對整個過程的復(fù)雜性深有體會,后續(xù)有機會也會分享出來。

          業(yè)務(wù)邏輯復(fù)雜

          循環(huán)調(diào)用

          這種情況,一般都循環(huán)調(diào)用同一段代碼,每次循環(huán)的邏輯一致,前后不關(guān)聯(lián)。比如說,我們要初始化一個列表,預(yù)置12個月的數(shù)據(jù)給前端:
          List<Model> list = new ArrayList<>();
          for(int i = 0 ; i < 12 ; i ++) {
              Model model = calOneMonthData(i); // 計算某個月的數(shù)據(jù),邏輯比較復(fù)雜,難以批量計算,效率也無法很高
              list.add(model);
          }
          這種顯然每個月的數(shù)據(jù)計算相互都是獨立的,我們完全可以采用多線程方式進行:
          // 建立一個線程池,注意要放在外面,不要每次執(zhí)行代碼就建立一個,具體線程池的使用就不展開了
          public static ExecutorService commonThreadPool = new ThreadPoolExecutor(5, 5, 300L,
                  TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy());

          // 開始多線程調(diào)用
          List<Future<Model>> futures = new ArrayList<>();
          for(int i = 0 ; i < 12 ; i ++) {
              Future<Model> future = commonThreadPool.submit(() -> calOneMonthData(i););
              futures.add(future);
          }

          // 獲取結(jié)果
          List<Model> list = new ArrayList<>();
          try {
             for (int i = 0 ; i < futures.size() ; i ++) {
                list.add(futures.get(i).get());
             }
          } catch (Exception e) {
             LOGGER.error("出現(xiàn)錯誤:", e);
          }

          順序調(diào)用

          如果不是類似上面循環(huán)調(diào)用,而是一次次的順序調(diào)用,而且調(diào)用之間沒有結(jié)果上的依賴,那么也可以用多線程的方式進行,例如:
          順序調(diào)用
          代碼上看:
          A a = doA();
          B b = doB();

          C c = doC(a, b);

          D d = doD(c);
          E e = doE(c);

          return doResult(d, e);
          那么可用CompletableFuture解決
          CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> doA());
          CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> doB());
          CompletableFuture.allOf(futureA,futureB) // 等a b 兩個任務(wù)都執(zhí)行完成

          C c = doC(futureA.join(), futureB.join());

          CompletableFuture<D> futureD = CompletableFuture.supplyAsync(() -> doD(c));
          CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c));
          CompletableFuture.allOf(futureD,futureE) // 等d e兩個任務(wù)都執(zhí)行完成

          return doResult(futureD.join(),futureE.join());

          這樣A B 兩個邏輯可以并行執(zhí)行,D E兩個邏輯可以并行執(zhí)行,最大執(zhí)行時間取決于哪個邏輯更慢。

          線程池設(shè)計不合理

          有的時候,即使我們使用了線程池讓任務(wù)并行處理,接口的執(zhí)行效率仍然不夠快,這種情況可能是怎么回事呢?
          這種情況首先應(yīng)該懷疑是不是線程池設(shè)計的不合理。我覺得這里有必要回顧一下線程池的三個重要參數(shù):核心線程數(shù)、最大線程數(shù)、等待隊列。這三個參數(shù)是怎么打配合的呢?當(dāng)線程池創(chuàng)建的時候,如果不預(yù)熱線程池,則線程池中線程為0。當(dāng)有任務(wù)提交到線程池,則開始創(chuàng)建核心線程。
          線程池設(shè)計不合理
          當(dāng)核心線程全部被占滿,如果再有任務(wù)到達,則讓任務(wù)進入等待隊列開始等待。
          讓任務(wù)進入等待隊列開始等待
          如果隊列也被占滿,則開始創(chuàng)建非核心線程運行。
          創(chuàng)建非核心線程運行
          如果線程總數(shù)達到最大線程數(shù),還是有任務(wù)到達,則開始根據(jù)線程池拋棄規(guī)則開始拋棄。
          根據(jù)線程池拋棄規(guī)則拋棄任務(wù)
          那么這個運行原理與接口運行時間有什么關(guān)系呢?
          • 核心線程設(shè)置過?。汉诵木€程設(shè)置過小則沒有達到并行的效果
          • 線程池公用,別的業(yè)務(wù)的任務(wù)執(zhí)行時間太長,占用了核心線程,另一個業(yè)務(wù)的任務(wù)到達就直接進入了等待隊列
          • 任務(wù)太多,以至于占滿了線程池,大量任務(wù)在隊列中等待
          在排查的時候,只要找到了問題出現(xiàn)的原因,那么解決方式也就清楚了,無非就是調(diào)整線程池參數(shù),按照業(yè)務(wù)拆分線程池等等。

          鎖設(shè)計不合理

          鎖設(shè)計不合理一般有兩種:鎖類型使用不合理 or 鎖過粗。
          鎖類型使用不合理的典型場景就是讀寫鎖。也就是說,讀是可以共享的,但是讀的時候不能對共享變量寫;而在寫的時候,讀寫都不能進行。在可以加讀寫鎖的時候,如果我們加成了互斥鎖,那么在讀遠遠多于寫的場景下,效率會極大降低。
          鎖過粗則是另一種常見的鎖設(shè)計不合理的情況,如果我們把鎖包裹的范圍過大,則加鎖時間會過長,例如:
          public synchronized void doSome() {
              File f = calData();
              uploadToS3(f);
              sendSuccessMessage();
          }
          這塊邏輯一共處理了三部分,計算、上傳結(jié)果、發(fā)送消息。顯然上傳結(jié)果和發(fā)送消息是完全可以不加鎖的,因為這個跟共享變量根本不沾邊。因此完全可以改成:
          public void doSome() {
              File f = null;
              synchronized(this) {
                  f = calData();
              }
              uploadToS3(f);
              sendSuccessMessage();
          }

          機器問題(fullGC,機器重啟,線程打滿)

          造成這個問題的原因非常多,筆者就遇到了定時任務(wù)過大引起fullGC,代碼存在線程泄露引起RSS內(nèi)存占用過高進而引起機器重啟等待諸多原因。需要結(jié)合各種監(jiān)控和具體場景具體分析,進而進行大事務(wù)拆分、重新規(guī)劃線程池等等工作

          萬金油解決方式

          萬金油這個形容詞是從我們單位某位老師那里學(xué)來的,但是筆者覺得非常貼切。這些萬金油解決方式往往能解決大部分的接口緩慢的問題,而且也往往是我們解決接口效率問題的最終解決方案。當(dāng)我們實在是沒有辦法排查出問題,或者實在是沒有優(yōu)化空間的時候,可以嘗試這種萬金油的方式。

          緩存

          緩存是一種空間換取時間的解決方案,是在高性能存儲介質(zhì)上(例如:內(nèi)存、SSD硬盤等)存儲一份數(shù)據(jù)備份。當(dāng)有請求打到服務(wù)器的時候,優(yōu)先從緩存中讀取數(shù)據(jù)。如果讀取不到,則再從硬盤或通過網(wǎng)絡(luò)獲取數(shù)據(jù)。由于內(nèi)存或SSD相比硬盤或網(wǎng)絡(luò)IO的效率高很多,則接口響應(yīng)速度會變快非常多。緩存適合于應(yīng)用在數(shù)據(jù)讀遠遠大于數(shù)據(jù)寫,且數(shù)據(jù)變化不頻繁的場景中。從技術(shù)選型上看,有這些:
          • 簡單的map
          • guava等本地緩存工具包
          • 緩存中間件:redis、tairmemcached
          當(dāng)然,memcached現(xiàn)在用的很少了,因為相比于redis他不占優(yōu)勢。tair則是阿里開發(fā)的一個分布式緩存中間件,他的優(yōu)勢是理論上可以在不停服的情況下,動態(tài)擴展存儲容量,適用于大數(shù)據(jù)量緩存存儲。相比于單機redis緩存當(dāng)然有優(yōu)勢,而他與可擴展Redis集群的對比則需要進一步調(diào)研。
          進一步的,當(dāng)前緩存的模型一般都是key-value模型。如何設(shè)計key以提高緩存的命中率是個大學(xué)問,好的key設(shè)計和壞的key設(shè)計所提升的性能差別非常大。而且,key設(shè)計是沒有一定之規(guī)的,需要結(jié)合具體的業(yè)務(wù)場景去分析。各個大公司分享出來的相關(guān)文章,緩存設(shè)計基本上是最大篇幅。

          回調(diào) or 反查

          這種方式往往是業(yè)務(wù)上的解決方式,在訂單或者付款系統(tǒng)中應(yīng)用的比較多。舉個例子:當(dāng)我們付款的時候,需要調(diào)用一個專門的付款系統(tǒng)接口,該系統(tǒng)經(jīng)過一系列驗證、存儲工作后還要調(diào)用銀行接口以執(zhí)行付款。由于付款這個動作要求十分嚴謹,銀行側(cè)接口執(zhí)行可能比較緩慢,進而拖累整個付款接口性能。這個時候我們就可以采用fast success的方式:當(dāng)必要的校驗和存儲完成后,立即返回success,同時告訴調(diào)用方一個中間態(tài)“付款中”。而后調(diào)用銀行接口,當(dāng)獲得支付結(jié)果后再調(diào)用上游系統(tǒng)的回調(diào)接口返回付款的最終結(jié)果“成果”or“失敗”。這樣就可以異步執(zhí)行付款過程,提升付款接口效率。當(dāng)然,為了防止多業(yè)務(wù)方接入的時候回調(diào)接口不統(tǒng)一,可以把結(jié)果拋進kafka,讓調(diào)用方監(jiān)聽自己的結(jié)果。
          fast success

          結(jié)語

          本文是筆者對工作中遇到的性能優(yōu)化問題的一個簡單的總結(jié),可能有不完備的地方,歡迎大家討論交流。


          到此文章就結(jié)束了。Java架構(gòu)師必看一個集公眾號、小程序、網(wǎng)站(3合1的文章平臺,給您架構(gòu)路上一臂之力,javajgs.com)。如果今天的文章對你在進階架構(gòu)師的路上有新的啟發(fā)和進步,歡迎轉(zhuǎn)發(fā)給更多人。歡迎加入架構(gòu)師社區(qū)技術(shù)交流群,眾多大咖帶你進階架構(gòu)師,在后臺回復(fù)“加群”即可入群。



          這些年小編給你分享過的干貨


          1.idea永久激活碼(親測可用)

          2.優(yōu)質(zhì)ERP系統(tǒng)帶進銷存財務(wù)生產(chǎn)功能(附源碼)

          3.優(yōu)質(zhì)SpringBoot帶工作流管理項目(附源碼)

          4.最好用的OA系統(tǒng),拿來即用(附源碼)

          5.SBoot+Vue外賣系統(tǒng)前后端都有(附源碼

          6.SBoot+Vue可視化大屏拖拽項目(附源碼)


          轉(zhuǎn)發(fā)在看就是最大的支持??

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  草美女视频网站 | 69视频在线播放 | 精品久久久久黄色片 | 欧美乱伦AA片 | 亚州在线观看视频 |