從2s最佳化到0.1s

苏三说技术發表於2024-11-21

前言

分類樹查詢功能,在各個業務系統中可以說隨處可見,特別是在電商系統中。

但就是這樣一個簡單的分類樹查詢功能,我們卻最佳化了5次。

到底是怎麼回事呢?

背景

我們的網站使用了SpringBoot推薦的模板引擎:Thymeleaf,進行動態渲染。

它是一個XML/XHTML/HTML5模板引擎,可用於Web與非Web環境中的應用開發。

它提供了一個用於整合SpringMVC的可選模組,在應用開發中,我們可以使用Thymeleaf來完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。

前端開發寫好Thymeleaf的模板檔案,呼叫後端介面獲取資料,進行動態繫結,就能把想要的內容展示給使用者。

由於當時這個是從0-1的新專案,為了開快速開發功能,我們第一版介面,直接從資料庫中查詢分類資料,組裝成分類樹,然後返回給前端。

透過這種方式,簡化了資料流程,快速把整個頁面功能調通了。

第1次最佳化

我們將該介面部署到dev環境,剛開始沒啥問題。

隨著開發人員新增的分類越來越多,很快就暴露出效能瓶頸。

我們不得不做最佳化了。

我們第一個想到的是:加Redis快取

流程圖如下:

圖片

於是暫時這樣最佳化了一下:

  1. 使用者訪問介面獲取分類樹時,先從Redis中查詢資料。
  2. 如果Redis中有資料,則直接資料。
  3. 如果Redis中沒有資料,則再從資料庫中查詢資料,拼接成分類樹返回。
  4. 將從資料庫中查到的分類樹的資料,儲存到Redis中,設定過期時間5分鐘。
  5. 將分類樹返回給使用者。

我們在Redis中定義一個了key,value是一個分類樹的json格式轉換成了字串,使用簡單的key/value形式儲存資料。

經過這樣最佳化之後,dev環境的聯調和自測順利完成了。

第2次最佳化

我們將這個功能部署到st環境了。

剛開始測試同學沒有發現什麼問題,但隨著後面不斷地深入測試,隔一段時間就出現一次首頁訪問很慢的情況。

於是,我們馬上進行了第2次最佳化。

我們決定使用Job定期非同步更新分類樹到Redis中,在系統上線之前,會先生成一份資料。

當然為了保險起見,防止Redis在哪條突然掛了,之前分類樹同步寫入Redis的邏輯還是保留。

於是,流程圖改成了這樣:

圖片

增加了一個job每隔5分鐘執行一次,從資料庫中查詢分類資料,封裝成分類樹,更新到Redis快取中。

其他的流程保持不變。

此外,Redis的過期時間之前設定的5分鐘,現在要改成永久。

透過這次最佳化之後,st環境就沒有再出現過分類樹查詢的效能問題了。

第3次最佳化

測試了一段時間之後,整個網站的功能快要上線了。

為了保險起見,我們需要對網站首頁做一次壓力測試。

果然測出問題了,網站首頁最大的qps是100多,最後發現是每次都從Redis獲取分類樹導致的網站首頁的效能瓶頸。

我們需要做第3次最佳化。

該怎麼最佳化呢?

答:加記憶體快取。

如果加了記憶體快取,就需要考慮資料一致性問題。

記憶體快取是儲存在伺服器節點上的,不同的伺服器節點更新的頻率可能有點差異,這樣可能會導致資料的不一致性。

但分類本身是更新頻率比較低的資料,對於使用者來說不太敏感,即使在短時間內,使用者看到的分類樹有些差異,也不會對使用者造成太大的影響。

因此,分類樹這種業務場景,是可以使用記憶體快取的。

於是,我們使用了Spring推薦的caffine作為記憶體快取。

改造後的流程圖如下:圖片

  1. 使用者訪問介面時改成先從本地快取分類數查詢資料。
  2. 如果本地快取有,則直接返回。
  3. 如果本地快取沒有,則從Redis中查詢資料。
  4. 如果Redis中有資料,則將資料更新到本地快取中,然後返回資料。
  5. 如果Redis中也沒有資料(說明Redis掛了),則從資料庫中查詢資料,更新到Redis中(萬一Redis恢復了呢),然後更新到本地快取中,返回返回資料。

需要注意的是,需要改本地快取設定一個過期時間,這裡設定的5分鐘,不然的話,沒辦法獲取新的資料。

這樣最佳化之後,再次做網站首頁的壓力測試,qps提升到了500多,滿足上線要求。

第4次最佳化

之後,這個功能順利上線了。

使用了很長一段時間沒有出現問題。

兩年後的某一天,有使用者反饋說,網站首頁有點慢。

我們排查了一下原因發現,分類樹的資料太多了,一次性返回了上萬個分類。

原來在系統上線的這兩年多的時間內,運營同學在系統後臺增加了很多分類。

我們需要做第4次最佳化。

這時要如何最佳化呢?

限制分類樹的數量?

答:也不太現實,目前這個業務場景就是有這麼多分類,不能讓使用者選擇不到他想要的分類吧?

這時我們想到最快的辦法是開啟nginxGZip功能。

讓資料在傳輸之前,先壓縮一下,然後進行傳輸,在使用者瀏覽器中,自動解壓,將真實的分類樹資料展示給使用者。

之前呼叫介面返回的分類樹有1MB的大小,最佳化之後,介面返回的分類樹的大小是100Kb,一下子縮小了10倍。

這樣簡單的最佳化之後,效能提升了一些。

第5次最佳化

經過上面最佳化之後,使用者很長一段時間都沒有反饋效能問題。

但有一天公司同事在排查Redis中大key的時候,揪出了分類樹。之前的分類樹使用key/value的結構儲存資料的。

我們不得不做第5次最佳化。

為了最佳化在Redis中儲存資料的大小,我們首先需要對資料進行瘦身。

只儲存需要用到的欄位。

例如:

@AllArgsConstructor
@Data
public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List<Category> children;
}

  

像這個分類物件中inDate、inUserId和inUserName欄位是可以不用儲存的。

修改自動名稱。

例如:

@AllArgsConstructor
@Data
public class Category {
    /**
     * 分類編號
     */
    @JsonProperty("i")
    private Long id;

    /**
     * 分類層級
     */
    @JsonProperty("l")
    private Integer level;

    /**
     * 分類名稱
     */
    @JsonProperty("n")
    private String name;

    /**
     * 父分類編號
     */
    @JsonProperty("p")
    private Long parentId;

    /**
     * 子分類列表
     */
    @JsonProperty("c")
    private List<Category> children;
}

  

由於在一萬多條資料中,每條資料的欄位名稱是固定的,他們的重複率太高了。

由此,可以在json序列化時,改成一個簡短的名稱,以便於返回更少的資料大小。

這還不夠,需要對儲存的資料做壓縮。

之前在Redis中儲存的key/value,其中的value是json格式的字串。

其實RedisTemplate支援,value儲存byte陣列

先將json字串資料用GZip工具類壓縮成byte陣列,然後儲存到Redis中。

再獲取資料時,將byte陣列轉換成json字串,然後再轉換成分類樹。

這樣最佳化之後,儲存到Redis中的分類樹的資料大小,一下子減少了10倍,Redis的大key問題被解決了。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回覆:進大廠,可以免費獲取我最近整理的10萬字的面試寶典,好多小夥伴靠這個寶典拿到了多家大廠的offer。

相關文章