天天看點

15個接口性能優化技巧,助你消滅慢代碼

作者:dbaplus社群

作為後端研發同學為了幾兩碎銀,沒日沒夜周旋于各種人、各種事上。

如果你要想成長的更快,就要學會歸納總結,找到規律,并且善用這些規律。

就比如工作,雖然事情很多、也很繁瑣,但如果按照性質歸下類,我覺得可以分為兩大類:

  • 業務類,如:産品要做一個紅包活動,下周一就要上線,于是研發同學就吭哧吭哧,周末加班不睡覺也要趕出來
  • 技術類,如:架構更新、系統優化等,這類事情對技術能力有一定要求,通常要求有一定的項目經驗的同學來 owner

關于業務類的内容很大程度依賴于産品同學的節奏,研發更多是被動角色,我們能做的是就是多跟産品聊天,「實時」了解産品的最新動向,培養自己的業務 sense,給自己多預留一定的buffer時間可以去做技術調研、技術儲備。

工作過一段時間同學一般都經曆過,産品變化節奏很快,經常都是倒排時間,讓研發苦不堪言。

至于技術類,相對就比較溫和的多了,不過也非常考驗研發的技術實力。

今天,我們就來聊下關于接口性能優化有哪些技巧?

一、本地緩存

本地緩存,最大的優點是應用和cache是在同一個程序内部,請求緩存非常快速,沒有過多的網絡開銷等,在單應用不需要叢集支援或者叢集情況下各節點無需互相通知的場景下使用本地緩存較合适。缺點也是因為緩存跟應用程式耦合,多個應用程式無法直接的共享緩存,各應用或叢集的各節點都需要維護自己的單獨緩存,對記憶體是一種浪費。

常用的本地緩存架構有 Guava、Caffeine 等,都是些單獨的jar包 ,直接導入到工程裡即可使用。

我們可以根據自己的需要靈活選擇想要哪個架構。

15個接口性能優化技巧,助你消滅慢代碼

本地緩存适用兩種場景:

  • 對緩存内容時效性要求不高,能接受一定的延遲,可以設定較短過期時間,被動失效更新保持資料的新鮮度。
  • 緩存的内容不會改變。比如:訂單号與uid的映射關系,一旦建立就不會發生改變。

注意問題:

  • 記憶體 Cache 資料條目上限控制,避免記憶體占用過多導緻應用癱瘓;
  • 記憶體中的資料移出政策;
  • 雖然實作簡單,但潛在的坑比較多,最好選擇一些成熟的開源架構。

二、分布式緩存

本地緩存的使用很容易讓你的應用伺服器帶上“狀态”,而且容易受記憶體大小的限制。

分布式緩存借助分布式的概念,叢集化部署,獨立運維,容量無上限,雖然會有網絡傳輸的損耗,但這1~2ms的延遲相比其更多優勢完成可以忽略。

優秀的分布式緩存系統有大家所熟知的 Memcached 、Redis。對比關系型資料庫和緩存存儲,其在讀和寫性能上的差距可謂天壤之别,redis單節點已經可以做到 8W+ QPS。設計方案時盡量把讀寫壓力從資料庫轉移到緩存上,有效保護脆弱的關系型資料庫。

注意問題:

  • 緩存的命中率,如果太低無法起到抗壓的作用,壓力還是壓到了下遊的存儲層;
  • 緩存的空間大小,這個要根據具體業務場景來評估,防止空間不足,導緻一些熱點資料被置換出去;
  • 緩存資料的一緻性;
  • 緩存的快速擴容問題;
  • 緩存的接口平均RT,最大RT,最小RT;
  • 緩存的QPS;
  • 網絡出口流量;
  • 用戶端連接配接數。

三、并行化

梳理業務流程,畫出時序圖,厘清楚哪些是串行?哪些是并行?充分利用多核 CPU 的并行化處理能力。

如下圖所示,存在上下文依賴的采用串行處理,否則采用并行處理。

15個接口性能優化技巧,助你消滅慢代碼

JDK 的 CompletableFuture 提供了非常豐富的API,大約有50種 處理串行、并行、組合以及處理錯誤的方法,可以滿足我們的場景需求。

四、異步化

一個接口的 RT 響應時間是由内部業務邏輯的複雜度決定的,執行的流程約簡單,那接口的耗費時間就越少。

是以,普遍做法就是将接口内部的非核心邏輯剝離出來,異步化來執行。

下圖是一個電商的建立訂單接口,建立訂單記錄并插入資料庫是我們的核心訴求,至于後續的使用者通知,如:給使用者發個短信等,如果失敗,并不影響主流程的完成。

我們會将這些操作從主流程中剝離出來。

15個接口性能優化技巧,助你消滅慢代碼

業務的普遍做法就是,下單成功後,發送一條異步消息到MQ 伺服器,由消費端監聽 topic,異步消費執行,通過 釋出/訂閱 模式也能支援一些新的消費任務的快速接入。

五、池化技術

TCP 三次握手非常耗費性能,是以我們引入了 Keep-Alive 長連接配接,避免頻繁的建立、銷毀連接配接。

池化技術也是類似道理,将很多能重複使用的對象緩存起來,放到一個池子裡,用的時候去申請一個執行個體對象 ,用完後再放回池子裡。

池化技術的核心是資源的“預配置設定”和“循環使用”,常見的池化技術的使用有:線程池、記憶體池、資料庫連接配接池、HttpClient 連接配接池等。

連接配接池的幾個重要參數:最小連接配接數、空閑連接配接數、最大連接配接數。

比如建立一個線程池:

new ThreadPoolExecutor(3, 15, 5, TimeUnit.MINUTES,
    new ArrayBlockingQueue<>(10),
    new ThreadFactoryBuilder().setNameFormat("data-thread-%d").build(),
    (r, executor) -> {
        if (r instanceof BaseRunnable) {
            ((BaseRunnable) r).rejectedExecute();
        }
    });           

六、分庫分表

MySQL的底層 innodb 存儲引擎采用 B+ 樹結構,三層結構支援千萬級的資料存儲。

當然,現在網際網路的使用者基數非常大,這麼大的使用者量,單表通常很難支撐業務需求,将一個大表水準拆分成多張結構一樣的實體表,可以極大緩解存儲、通路壓力。

15個接口性能優化技巧,助你消滅慢代碼

分庫分表也可能會帶入很多問題:

  • 分庫分表後,資料在分表内産生資料傾斜;
  • 如何建立全局性的唯一主鍵id;
  • 資料如何路由到哪一個分片。

每一個問題展開都要花費很長篇幅來講解,這裡主要講接口性能優化的方案彙總,就不展開細講了。

關于分庫分表,市場受歡迎的開源架構是 sharding-jdbc,目前已經捐贈給Apache并啟動孵化。

七、SQL 優化

雖然有了分庫分表,從存儲次元可以減少很大壓力,但「富不過三代」,我們還是要學會精打細算,就比如所有的資料庫操作都是通過 SQL 來執行。

一個不好的SQL會對接口性能産生很大影響。

比如:

  • 搞了個深度翻頁,每次資料庫引擎都要預查非常多的資料;
  • 索引缺失,走了全表掃描;
  • 一條 SQL 一次查詢 幾萬條資料。

SQL 優化的經驗非常多,比如:

  • SQL 查詢時,盡量不要使用 select * ,而是 select 具體字段;
  • 如果隻有一條查詢結果(或者最大值、最小值),建議使用 limit 1;
  • 索引不宜太多,一般控制在 5個以内;
  • where 語句中盡量避免使用 or來連接配接條件。or 可能會導緻索引失效,進而全表掃描
  • 索引盡量避免建在有大量重複資料的字段上,如:性别;
  • where 、 order by 涉及的列上建索引,避免全表掃描;
  • 更多.....

SQL 優化的内容非常多,這裡就不展開了。

八、預先計算

有很多業務的計算邏輯比較複雜,比如頁面要展示一個網站的 PV、微信的拼手氣紅包等。

如果在使用者通路接口的瞬間觸發計算邏輯,而這些邏輯計算的耗時通常比較長,很難滿足使用者的實時性要求。

一般我們都是提前計算,然後将算好的資料預熱到緩存中,接口通路時,隻需要讀緩存即可

是不是一下子就快了很多。

15個接口性能優化技巧,助你消滅慢代碼

九、事務相關

很多業務邏輯有事務要求,針對多個表的寫操作要保證事務特性。

但事務本身又特别耗費性能,為了能盡快結束,不長時間占用資料庫連接配接資源,我們一般要減少事務的範圍。

将很多查詢邏輯放到事務外部處理。

另外在事務内部,一般不要進行遠端的 RPC 接口通路,一般占用的時間比較長。

十、海量資料處理

如果資料量過大,除了采用關系型資料庫的分庫分表外,我們還可以采用 NoSQL

如:MongoDB、Hbase、Elasticsearch、TiDB。

NoSQL 采用分區架構,對資料海量存儲能較好的支援,但是事務方面可能沒那麼友好。

每一個 NoSQL 架構都有自己的特色,有支援 搜尋的、有列式存儲、有文檔存儲,大家可以根據自己的業務場景選擇合适的架構。

十一、批量讀寫

當下的計算機CPU處理速度還是很多的,而 IO 一般是個瓶頸,如:磁盤IO、網絡IO。

有這麼一個場景,查詢 100 個人的賬戶餘額?

有兩個設計方案:

  • 方案一:開單次查詢接口,調用方内部循環調用 100 次。
  • 方案二:服務提供方開一個批量查詢接口,調用方隻需查詢 1 次。

你覺得那種方案更好?

答案不言而喻,肯定是方案二。

資料庫的寫操作也是一樣道理,為了提高性能,我們一般都是采用批量更新。

十二、鎖的粒度

并發業務,為了防止資料的并發更新對資料的正确性産生幹擾,我們通常是采用 加鎖 ,涉及獨享資源每次隻能是一個線程來處理。

問題點在于,鎖是成對出現的,有加鎖就是釋放鎖。

對于非競争資源,我們沒有必要圈在鎖内部,會嚴重影響系統的并發能力。

控制鎖的範圍是我們要考慮的重點。

十三、上下文傳遞

本人所帶團隊對小夥伴有要求,代碼必須要有 code review 環節,review 同學代碼經常發現一個問題。

當需要一個資料時,如果沒有調 RPC 接口去查,比如想使用者資訊這種通用型接口

因為前面要用,肯定已經查過。但是我們知道方法的調用都是以棧幀的形式來傳遞,随着一個方法執行完畢而出棧,方法内部的局部變量也就被回收了。

後面如果又要用到這個資訊,隻能重新去查。

如果能定義一個Context 上下文對象,将一些中間資訊存儲并傳遞下來,會大大減輕後面流程的再次查詢壓力。

十四、空間大小

如何建立一個集合,這還不簡單,很快我們就寫出下面代碼:

List<String> lists = Lists.newArrayList();           

如果說,要往裡面插入 1000000 個元素,有沒有更好的方式?

我們做個試驗:

  • 場景一
15個接口性能優化技巧,助你消滅慢代碼

結果:1000000 次插入 List,花費時間:154。

  • 場景二
15個接口性能優化技巧,助你消滅慢代碼

結果:1000000 次插入 List,花費時間:134。

如果我們預先知道集合要存儲多少元素,初始化集合時盡量指定大小,尤其是容量較大的集合。

ArrayList 初始大小是 10,超過門檻值會按 1.5 倍大小擴容,涉及老集合到新集合的資料拷貝,浪費性能。

十五、查詢優化

避免一次從 DB 中查詢大量的資料到記憶體中,可能會導緻記憶體不足,建議采用分批、分頁查詢。

作者介紹

Tom哥,前阿裡P7技術專家,offer收割機,參加多次淘寶雙11大促活動。

作者丨Tom哥

來源丨公衆号:微觀技術(ID:weiguanjishu)

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]

關于我們

dbaplus社群是圍繞Database、BigData、AIOps的企業級專業社群。資深大咖、技術幹貨,每天精品原創文章推送,每周線上技術分享,每月線下技術沙龍,每季度Gdevops&DAMS行業大會。

關注公衆号【dbaplus社群】,擷取更多原創技術文章和精選工具下載下傳

繼續閱讀