前言
分類樹
查詢功能,在各個業務系統中可以說隨處可見,特別是在電商系統中。
但就是這樣一個簡單的分類樹查詢功能,我們卻最佳化了5
次。
到底是怎麼回事呢?
背景
我們的網站使用了SpringBoot
推薦的模板引擎:Thymeleaf
,進行動態渲染。
它是一個XML/XHTML/HTML5模板引擎,可用於Web與非Web環境中的應用開發。
它提供了一個用於整合SpringMVC的可選模組,在應用開發中,我們可以使用Thymeleaf來完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。
前端開發寫好Thymeleaf的模板檔案,呼叫後端介面獲取資料,進行動態繫結,就能把想要的內容展示給使用者。
由於當時這個是從0-1的新專案,為了開快速開發功能,我們第一版介面,直接從資料庫中查詢分類
資料,組裝成分類樹
,然後返回給前端。
透過這種方式,簡化了資料流程,快速把整個頁面功能調通了。
第1次最佳化
我們將該介面部署到dev環境,剛開始沒啥問題。
隨著開發人員新增的分類越來越多,很快就暴露出效能瓶頸。
我們不得不做最佳化了。
我們第一個想到的是:加Redis快取
。
流程圖如下:
於是暫時這樣最佳化了一下:
- 使用者訪問介面獲取分類樹時,先從Redis中查詢資料。
- 如果Redis中有資料,則直接資料。
- 如果Redis中沒有資料,則再從資料庫中查詢資料,拼接成分類樹返回。
- 將從資料庫中查到的分類樹的資料,儲存到Redis中,設定過期時間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
作為記憶體快取。
改造後的流程圖如下:
- 使用者訪問介面時改成先從本地快取分類數查詢資料。
- 如果本地快取有,則直接返回。
- 如果本地快取沒有,則從Redis中查詢資料。
- 如果Redis中有資料,則將資料更新到本地快取中,然後返回資料。
- 如果Redis中也沒有資料(說明Redis掛了),則從資料庫中查詢資料,更新到Redis中(萬一Redis恢復了呢),然後更新到本地快取中,返回返回資料。
需要注意的是,需要改本地快取設定一個過期時間,這裡設定的5分鐘,不然的話,沒辦法獲取新的資料。
這樣最佳化之後,再次做網站首頁的壓力測試,qps提升到了500多,滿足上線要求。
第4次最佳化
之後,這個功能順利上線了。
使用了很長一段時間沒有出現問題。
兩年後的某一天,有使用者反饋說,網站首頁有點慢。
我們排查了一下原因發現,分類樹的資料太多了,一次性返回了上萬個分類。
原來在系統上線的這兩年多的時間內,運營同學在系統後臺增加了很多分類。
我們需要做第4次最佳化。
這時要如何最佳化呢?
限制分類樹的數量?
答:也不太現實,目前這個業務場景就是有這麼多分類,不能讓使用者選擇不到他想要的分類吧?
這時我們想到最快的辦法是開啟nginx
的GZip
功能。
讓資料在傳輸之前,先壓縮一下,然後進行傳輸,在使用者瀏覽器
中,自動解壓,將真實的分類樹資料展示給使用者。
之前呼叫介面返回的分類樹有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。