博客 / 詳情

返回

搜索數據庫表的性能優化過程

問題背景

做一個數據庫表查看、標註與分析的工具軟件。

\(Table\)是數據庫中表的信息(information_schema.tables);\(Documentation\)\(Table\)的數據字典文檔,存儲在本地文件中;\(Annotation\)是對\(Table\)的額外標註信息,存儲在另一個數據庫中。每一條\(Table\),最多關聯到一條\(Documentation\)和一條\(Annotation\)

現在想搜索\(Table\)。前端向後端提供3個參數,搜索關鍵詞列表、當前頁碼、每頁條數;後端的搜索邏輯是,如果一條完整數據(\(Table\)+\(Documentation\)+\(Annotation\))包含所有搜索關鍵詞,則將\(Table\)加入搜索結果中。

\(Table\)的數量目前為6000+,要做到秒級搜索。

初步實現

因為跨數據源,所以不能簡單連表查詢。

對於每個\(Table\),查出\(Documentation\)\(Annotation\),然後將\(Table\)\(Documentation\)\(Annotation\)中要搜索的字段值取出來,用空格隔開拼接為字符串,形如"Table字段值 Documentation字段值 Annotation字段值",我們稱之為\(SearchKey\)(搜索鍵)。如果每個關鍵詞都包含在\(SearchKey\)中,則將\(Table\)加入搜索結果。

搜索時,先獲取所有\(Table\),然後遍歷每個\(Table\),獲取\(SearchKey\)並判斷是否加入搜索結果。

為了提高速度,用Redis緩存\(Table\)對應的\(SearchKey\)

分析數據情況:

  • \(Table\)只增、不刪、不改,因此,搜索時要重新獲取所有\(Table\),確保搜索到新\(Table\);不必考慮驅逐(evict)\(SearchKey\)的緩存。
  • \(Documentation\)不增、不刪、不改,因此,不必考慮驅逐\(SearchKey\)的緩存。
  • \(Annotation\)增、刪、改,因此,要在\(Annotation\)增刪改之後驅逐對應\(SearchKey\)的緩存,確保搜索到\(Annotation\)的最新信息。

實測結果:

  • 實現了功能,支持同時按\(Table\)\(Documentation\)\(Annotation\)的字段搜索。
  • 有性能問題,即使緩存已經全部完成,但每次搜索都要耗時30s左右,原因是6000+個\(Table\)遍歷從Redis獲取\(SearchKey\),每次耗時1~15ms,累計耗時非常長。

第一次性能優化

優化緩存策略。

獲取所有\(Table\)後,構建\(SearchKeyMap\)\(Table\)\(SearchKey\)),然後將\(SearchKeyMap\)緩存,這樣,下一次搜索時,只需要從Redis獲取一次,提高傳輸效率。

為了確保搜索到新\(Table\),緩存\(SearchKeyMap\)時將\(Table\)列表的長度作為緩存鍵,如果新增了\(Table\),則\(SearchKeyMap\)不會命中緩存,而是重新構建。

為了減少構建\(SearchKeyMap\)的時間,仍然保留單個\(SearchKey\)的緩存,仍然在\(Annotation\)增刪改之後驅逐單個\(SearchKey\)的緩存,但不同的是,還要同時驅逐\(SearchKeyMap\)的緩存。

實測結果:

  • 性能提升明顯,在緩存全部完成的情況下,搜索耗時降至1.3s左右。
  • 仍然有性能問題,對一個\(Annotation\)做了增刪改,會驅逐整個\(SearchKeyMap\)緩存,重建\(SearchKeyMap\)就又回到了遍歷\(Table\)的情況,仍然要耗時30s左右。

第二次性能優化

優化緩存策略。

取消單個\(SearchKey\)的緩存,只緩存\(SearchKeyMap\)

搜索\(Table\)時,要獲取\(SearchKeyMap\)。先獲取現有的\(SearchKeyMap\)緩存(固定緩存鍵,不再使用列表長度作為緩存鍵;沒有緩存則取得空Map),然後遍歷\(Table\),如果\(Table\)不在\(SearchKeyMap\)中,則計算\(SearchKey\)並放入\(SearchKeyMap\)。這樣,第一次搜索時會計算每個\(Table\)\(SearchKey\),後續搜索就只需要計算新\(Table\)\(SearchKey\)

\(Annotation\)增刪改後,要更新\(SearchKeyMap\)。先獲取現有的\(SearchKeyMap\)緩存,然後重新計算指定\(Table\)\(SearchKey\)並放入\(SearchKeyMap\)。這樣,無需每次都重建整個\(SearchKeyMap\)

實測結果:\(Annotation\)增刪改後再搜索,耗時降至1.3s左右。

第三次性能優化

優化緩存實現方式。

既然現在只需要簡單地緩存一個\(SearchKeyMap\),那麼不一定要用Redis。

使用Redis作為緩存(RedisCacheManager),雖然內網通信快,但仍有網絡開銷。實測平均1092.9ms。

使用Map作為緩存(ConcurrentMapCacheManager),其他代碼完全不變。實測平均968.3ms。

修改代碼,直接用類中的Map字段作為緩存,省去緩存管理器的開銷。實測平均915.2ms。

可見,性能有提升,但幅度不大。由於軟件在開發中,要頻繁重新運行,Redis能保持緩存,Map不能,因此保持上一版方案不做修改。

第四次性能優化

第三次優化其實是盲目的,應該要用事實找出性能瓶頸。

對搜索過程計時分析發現,一次耗時1105ms的搜索,其中獲取所有\(Table\)耗時1028ms,佔比93%,是絕對的性能瓶頸。

思路1:先只獲取所有表名,而不是\(Table\)對象,如果表名對應的\(SearchKey\)匹配,再獲取\(Table\)。實測發現,如果匹配的表名很多(例如關鍵詞列表為空時),則即使有表名→\(Table\)的緩存(Redis實現),逐個獲取也遠遠慢於直接從數據庫一次性獲取。因此,此思路不可行

思路2:\(Table\)只增、不刪、不改,因此可以考慮增量獲取。緩存\(Table\)列表,每次獲取時跳過緩存的長度,只獲取增量部分。然而,information_schema.tables中沒有id,無法保證新\(Table\)一定排在最後。因此,此思路不可行

思路3:獲取所有\(Table\)説到底只是為了搜索到新\(Table\),如果能知道什麼時候新增了\(Table\),就可以放心地使用\(Table\)列表的緩存,或者從數據庫重新獲取。那麼怎麼知道?由於\(Table\)只增,所以可以用\(Table\)的數量判斷。緩存\(Table\)列表,每次先從數據庫查出數量(比直接查出\(Table\)列表明顯更快),如果數量與緩存一致,則用緩存,否則查庫。實測,此思路可行

實現思路3後,再次計時分析。無新增\(Table\)時,搜索耗時降至360ms左右(只查庫數量);有新增時,耗時升至1.5s左右(查庫數量+列表)。由於搜索\(Table\)的頻率遠遠高於新增\(Table\),因此,總體性能提升顯著。

總結

經過數次性能優化,在滿足功能的前提下,搜索時間從30s左右降至穩定0.4s左右,效果顯著。0.4s已經沒有緩慢感,性能優化工作可以結束了。

從上述優化過程可見,做優化要因地制宜,具體問題具體分析,選擇合適的策略;優化效果的衡量要以實測結果為準。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.