1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 8520字,需瀏覽 18分鐘

         ·

        2022-07-23 16:32

        往期熱門文章:

        1、3 步完成 Spring Boot 的日志脫敏
        2、線上MySQL的自增id用盡怎么辦?被面試官干趴下了!
        3、求求你別再用 System.currentTimeMillis() 統(tǒng)計代碼耗時了,真的太 Low 了!
        4、如何將 @Transactional 事務注解運用到爐火純青?
        5、不知道怎么解耦業(yè)務?Spring Event 了解一下!

        juejin.cn/post/7043423820543164453

        背景

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

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

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

        問題解決

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

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

        所謂的深度分頁問題,涉及到mysql分頁的原理。通常情況下,mysql的分頁是這樣寫的:
        select name,code from student limit 100,20
        含義當然就是從student表里查100到120這20條數(shù)據(jù),mysql會把前120條數(shù)據(jù)都查出來,拋棄前100條,返回20條。當分頁所以深度不大的時候當然沒問題,隨著分頁的深入,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卻不會告訴咱,需要咱自己分析。大體上,可能引起索引失效的原因有這幾個(可能不完全):
        慢查詢索引失效
        需要特別提出的是,關于字段區(qū)分性很差的情況,在加索引的時候就應該進行評估。如果區(qū)分性很差,這個索引根本就沒必要加。區(qū)分性很差是什么意思呢,舉幾個例子,比如:
        • 某個字段只可能有3個值,那這個字段的索引區(qū)分度就很低。
        • 再比如,某個字段大量為空,只有少量有值;
        • 再比如,某個字段值非常集中,90%都是1,剩下10%可能是2,3,4....
        進一步的,那如果不符合上面所有的索引失效的情況,但是mysql還是不使用對應的索引,是為啥呢?這個跟mysql的sql優(yōu)化有關,mysql會在sql優(yōu)化的時候自己選擇合適的索引,很可能是mysql自己的選擇算法算出來使用這個索引不會提升性能,所以就放棄了。這種情況,可以使用force index 關鍵字強制使用索引(建議修改前先實驗一下,是不是真的會提升查詢效率):
        select name,code from student force index(XXXXXX) where name = '天才' 
        其中xxxx是索引名。

        join過多 or 子查詢過多

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

        in的元素過多

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

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

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

        業(yè)務邏輯復雜

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

        這種情況,一般都循環(huán)調(diào)用同一段代碼,每次循環(huán)的邏輯一致,前后不關聯(lián)。比如說,我們要初始化一個列表,預置12個月的數(shù)據(jù)給前端:
        List<Model> list = new ArrayList<>();
        for(int i = 0 ; i < 12 ; i ++) {
            Model model = calOneMonthData(i); // 計算某個月的數(shù)據(jù),邏輯比較復雜,難以批量計算,效率也無法很高
            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);
        }

        // 獲取結果
        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)用之間沒有結果上的依賴,那么也可以用多線程的方式進行,例如:
        順序調(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 兩個任務都執(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兩個任務都執(zhí)行完成

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

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

        線程池設計不合理

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

        鎖設計不合理

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

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

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

        萬金油解決方式

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

        緩存

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

        回調(diào) or 反查

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

        結語

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

        往期熱門文章:

        1、線上MySQL的自增id用盡怎么辦?被面試官干趴下了!
        2、計算機專業(yè)會不會成為下一個土木?
        3、xxl-job驚艷的設計,怎能叫人不愛
        4、ArrayList#subList這四個坑,一不小心就中招
        5、面試官:大量請求 Redis 不存在的數(shù)據(jù),從而影響數(shù)據(jù)庫,該如何解決?
        6、MySQL 暴跌!
        7、超越 Xshell!號稱下一代 Terminal 終端神器,用完愛不釋手!
        8、IDEA 官宣全新默認 UI,太震撼了??!
        9、讓你直呼「臥槽」的 GitHub 項目!
        10、Kafka又笨又重,為啥不選Redis?

        瀏覽 98
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久激情国产 | 18 无套直看片 | 超污视频网站 | 国内精品在线一区二区 | 色五月色婷婷AV在线 | 老赵揉搓着娇妻的大乳视频 | 中国黄视频| 国产女人18毛片水18精品变态 | 伊人成色| 一级卖婬片aaaa |