Java技術突擊網站:http://www.susan.net.cn
分類樹
查詢功能,在各個業務系統中可以說隨處可見,特別是在電商系統中。
但就是這樣一個簡單的分類樹查詢功能,我們卻優化了5
次。
到底是怎麼回事呢?
我們的網站使用了SpringBoot
推薦的模板引擎:Thymeleaf
,進行動態渲染。
它是一個XML/XHTML/HTML5模板引擎,可用於Web與非Web環境中的應用開發。
它提供了一個用於整合SpringMVC的可選模組,在應用開發中,我們可以使用Thymeleaf來完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。
前端開發寫好Thymeleaf的模板檔案,呼叫後端介面獲取資料,進行動態繫結,就能把想要的內容展示給使用者。
由於當時這個是從0-1的新專案,為了開快速開發功能,我們第一版介面,直接從資料庫中查詢分類
資料,組裝成分類樹
,然後返回給前端。
通過這種方式,簡化了資料流程,快速把整個頁面功能調通了。
我們將該介面部署到dev環境,剛開始沒啥問題。
隨著開發人員新增的分類越來越多,很快就暴露出效能瓶頸。
我們不得不做優化了。
我們第一個想到的是:加Redis快取
。
流程圖如下:
於是暫時這樣優化了一下:
我們在Redis中定義一個了key,value是一個分類樹的json格式轉換成了字串,使用簡單的key/value形式儲存資料。
經過這樣優化之後,dev環境的聯調和自測順利完成了。
我們將這個功能部署到st環境了。
剛開始測試同學沒有發現什麼問題,但隨著後面不斷地深入測試,隔一段時間就出現一次首頁存取很慢的情況。
於是,我們馬上進行了第2次優化。
我們決定使用Job
定期非同步
更新分類樹到Redis中,在系統上線之前,會先生成一份資料。
當然為了保險起見,防止Redis在哪條突然掛了,之前分類樹同步寫入Redis的邏輯還是保留。
於是,流程圖改成了這樣:
增加了一個job每隔5分鐘執行一次,從資料庫中查詢分類資料,封裝成分類樹,更新到Redis快取中。
其他的流程保持不變。
此外,Redis的過期時間之前設定的5分鐘,現在要改成永久。
通過這次優化之後,st環境就沒有再出現過分類樹查詢的效能問題了。
測試了一段時間之後,整個網站的功能快要上線了。
為了保險起見,我們需要對網站首頁做一次壓力測試。
果然測出問題了,網站首頁最大的qps是100多,最後發現是每次都從Redis獲取分類樹導致的網站首頁的效能瓶頸。
我們需要做第3次優化。
該怎麼優化呢?
答:加記憶體快取。
如果加了記憶體快取,就需要考慮資料一致性問題。
記憶體快取是儲存在伺服器節點上的,不同的伺服器節點更新的頻率可能有點差異,這樣可能會導致資料的不一致性。
但分類本身是更新頻率比較低的資料,對於使用者來說不太敏感,即使在短時間內,使用者看到的分類樹有些差異,也不會對使用者造成太大的影響。
因此,分類樹這種業務場景,是可以使用記憶體快取的。
於是,我們使用了Spring推薦的caffine
作為記憶體快取。
改造後的流程圖如下:
需要注意的是,需要改本地快取設定一個過期時間,這裡設定的5分鐘,不然的話,沒辦法獲取新的資料。
這樣優化之後,再次做網站首頁的壓力測試,qps提升到了500多,滿足上線要求。
之後,這個功能順利上線了。
使用了很長一段時間沒有出現問題。
兩年後的某一天,有使用者反饋說,網站首頁有點慢。
我們排查了一下原因發現,分類樹的資料太多了,一次性返回了上萬個分類。
原來在系統上線的這兩年多的時間內,運營同學在系統後臺增加了很多分類。
我們需要做第4次優化。
這時要如何優化呢?
限制分類樹的數量?
答:也不太現實,目前這個業務場景就是有這麼多分類,不能讓使用者選擇不到他想要的分類吧?
這時我們想到最快的辦法是開啟nginx
的GZip
功能。
讓資料在傳輸之前,先壓縮一下,然後進行傳輸,在使用者瀏覽器
中,自動解壓,將真實的分類樹資料展示給使用者。
之前呼叫介面返回的分類樹有1MB的大小,優化之後,介面返回的分類樹的大小是100Kb,一下子縮小了10倍。
這樣簡單的優化之後,效能提升了一些。
經過上面優化之後,使用者很長一段時間都沒有反饋效能問題。
但有一天公司同事在排查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問題被解決了。
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維條碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。