在電商系統中,分類樹查詢是一個基礎且高頻的功能,然而這個看似簡單的功能背后卻隱藏著不小的性能挑戰。本文將分享我們在實際項目中對分類樹查詢功能進行五次優化的全過程,看如何將查詢耗時從 2 秒縮短至 0.1 秒,為用戶提供更流暢的體驗。
一、初始版本:從數據庫直接查詢
我們的項目采用 Spring Boot 框架,前端使用 Thymeleaf 模板引擎進行動態渲染。在項目初期,為了快速開發功能,分類樹查詢接口直接從數據庫中查詢分類數據,組裝成分類樹后返回給前端。
這種簡單直接的方式雖然快速實現了功能,但隨著分類數據的不斷增加,性能問題很快暴露出來。在開發環境中,當分類數量較多時,接口響應時間逐漸變長,甚至達到了 2 秒,嚴重影響了用戶體驗。
二、第一次優化:引入 Redis 緩存
面對性能瓶頸,我們首先想到的是添加 Redis 緩存。優化后的流程如下:
- 用戶訪問接口獲取分類樹時,先從 Redis 中查詢數據。
- 如果 Redis 中有數據,直接返回。
- 如果 Redis 中沒有數據,從數據庫中查詢數據,拼接成分類樹返回。
- 將從數據庫中查到的分類樹數據保存到 Redis 中,設置過期時間為 5 分鐘。
通過這種方式,大部分請求可以直接從 Redis 中獲取數據,減少了數據庫的訪問壓力。經過測試,開發環境的接口響應時間得到了明顯改善,聯調和自測順利完成。
三、第二次優化:異步定期更新緩存
將功能部署到測試環境后,初期測試沒有發現問題,但隨著測試的深入,隔一段時間就會出現首頁訪問很慢的情況。分析發現,當 Redis 緩存過期時,大量請求同時訪問數據庫,導致數據庫壓力過大,從而影響了性能。
為了解決這個問題,我們決定使用 Job 定期異步更新分類樹到 Redis 中。具體優化措施如下:
- 增加一個 Job,每隔 5 分鐘執行一次,從數據庫中查詢分類數據,封裝成分類樹,更新到 Redis 緩存中。
- 保留原來的分類樹同步寫入 Redis 的邏輯,以防止 Redis 突然掛掉。
- 將 Redis 的過期時間改為永久。
這次優化后,測試環境再也沒有出現分類樹查詢的性能問題。
四、第三次優化:添加內存緩存
在網站即將上線前,我們對首頁進行了壓力測試,發現最大 QPS 只有 100 多,性能瓶頸依然存在。經過分析,我們發現每次都從 Redis 獲取分類樹是導致性能問題的主要原因。
于是,我們決定添加內存緩存。考慮到分類數據更新頻率較低,即使不同服務器節點的內存緩存數據存在短暫的不一致,也不會對用戶造成太大影響,因此選擇使用 Spring 推薦的 Caffeine 作為內存緩存。優化后的流程如下:
- 用戶訪問接口時,先從本地內存緩存中查詢分類樹數據。
- 如果本地緩存有數據,直接返回。
- 如果本地緩存沒有數據,從 Redis 中查詢數據。
- 如果 Redis 中有數據,將數據更新到本地緩存中,然后返回。
- 如果 Redis 中也沒有數據(說明 Redis 掛了),從數據庫中查詢數據,更新到 Redis 中,然后更新到本地緩存中,返回數據。
- 設置本地緩存的過期時間為 5 分鐘,以便獲取新的數據。
這次優化效果顯著,再次進行壓力測試時,QPS 提升到了 500 多,滿足了上線要求。
五、第四次優化:開啟 GZip 壓縮
使用了很長一段時間都沒有出現問題。但兩年后的一天,有用戶反饋網站首頁有點慢。經過排查,我們發現分類樹的數據已經增加到了上萬個,一次性返回的數據量太大,導致網絡傳輸耗時較長。
針對這個問題,我們想到了開啟 Nginx 的 GZip 功能,讓數據在傳輸之前先進行壓縮。之前調用接口返回的分類樹數據大小為 1MB,開啟 GZip 壓縮后,數據大小縮小到了 100KB,一下子縮小了 10 倍,性能得到了明顯提升。
六、第五次優化:優化 Redis 存儲
在一次 Redis 大 key 排查中,分類樹數據被揪了出來。原來,我們一直使用簡單的 key/value 結構在 Redis 中保存分類樹數據,隨著分類數量的增加,這個 value 變得越來越大,成為了 Redis 中的大 key,影響了 Redis 的性能。
為了解決這個問題,我們從以下幾個方面進行了優化:
- 數據瘦身:只保存需要用到的字段,去除了如 inDate、inUserId 和 inUserName 等不必要的字段。
- 修改字段名稱:在 JSON 序列化時,將字段名稱改為簡短的名稱,減少數據量。例如,將 id 改為 i,name 改為 n 等。
- 數據壓縮:使用 GZip 工具類將 JSON 字符串壓縮成 byte 數組,然后保存到 Redis 中。獲取數據時,再將 byte 數組解壓并轉換成 JSON 字符串。
例如
@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;
}
例如
@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;
}
經過這些優化,保存到 Redis 中的分類樹數據大小減少了 10 倍,成功解決了 Redis 的大 key 問題。
七、優化成果總結
通過這五次優化,分類樹查詢的性能得到了顯著提升:
- 初始版本:響應時間約 2 秒
- 最終版本:響應時間約 0.1 秒
- QPS 從 100 多提升到 500 多
- 數據傳輸量從 1MB 減少到 100KB 左右
- Redis 中的數據存儲量減少了 10 倍
八、經驗啟示
- 性能優化是一個持續的過程:隨著業務的發展和數據量的增加,原來的優化措施可能會逐漸失效,需要持續關注性能問題并進行優化。
- 緩存策略的選擇很重要:根據數據的特點和業務需求,選擇合適的緩存策略,如 Redis 緩存、內存緩存等,并合理設置緩存過期時間。
- 數據壓縮不容忽視:在數據傳輸和存儲過程中,合理使用數據壓縮技術可以有效減少數據量,提高性能。
- 數據庫操作要謹慎:數據庫是系統的瓶頸之一,應盡量減少對數據庫的訪問,避免大量并發請求同時訪問數據庫。
- 代碼優化細節決定成敗:在實際開發中,一些看似微小的優化,如字段名稱的簡化、不必要字段的去除等,累積起來也能帶來顯著的性能提升。