Java中如何實現緩存穿透 分析解決方案

緩存穿透是指查詢一個數據庫中肯定不存在的數據,導致每次請求都打到數據庫,解決方案有:1. 使用布隆過濾器,通過bit數組和哈希函數高效判斷key是否存在,但有一定誤判率;2. 緩存空對象,在數據庫無數據時緩存空對象以減少后續請求;3. 接口層校驗,對請求參數進行合法性校驗,防止非法請求到達數據庫。

Java中如何實現緩存穿透 分析解決方案

緩存穿透,簡單來說,就是查詢一個數據庫里肯定不存在的數據。由于緩存中沒有,每次請求都會打到數據庫,如果這種請求量很大,數據庫就可能扛不住。

Java中如何實現緩存穿透 分析解決方案

解決方案:

Java中如何實現緩存穿透 分析解決方案

布隆過濾器:高效判斷 Key 是否存在

布隆過濾器是一種概率型數據結構,能告訴你“可能存在”或“肯定不存在”。 它的優點是空間效率和查詢效率都很高,缺點是有一定的誤判率(false positive)。

立即學習Java免費學習筆記(深入)”;

工作原理:

Java中如何實現緩存穿透 分析解決方案

  1. 初始化: 創建一個 bit 數組,所有位初始化為 0。定義 k 個不同的哈希函數。
  2. 添加元素: 將元素通過 k 個哈希函數計算出 k 個哈希值,然后將 bit 數組中對應的 k 個位置置為 1。
  3. 查詢元素: 將元素通過 k 個哈希函數計算出 k 個哈希值,檢查 bit 數組中對應的 k 個位置是否都為 1。如果都為 1,則“可能存在”,否則“肯定不存在”。

Java 實現示例(使用 guava):

import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels;  public class BloomFilterExample {      private static final int EXPECTED_INSERTIONS = 1000; // 預期的插入數量     private static final double FPP = 0.01; // 誤判率      private static BloomFilter<Integer> bloomFilter = BloomFilter.create(             Funnels.integerFunnel(),             EXPECTED_INSERTIONS,             FPP);      public static void main(String[] args) {         // 假設數據庫中存在 1 到 100 的數據         for (int i = 1; i <= 100; i++) {             bloomFilter.put(i);         }          // 測試不存在的數據         System.out.println("Checking if 101 exists: " + bloomFilter.mightContain(101)); // 可能返回 true (誤判) 或 false         System.out.println("Checking if 2 exists: " + bloomFilter.mightContain(2)); // 肯定返回 true     } }

使用場景:

  • 在緩存之前,先通過布隆過濾器判斷 Key 是否存在,如果不存在,直接返回,避免查詢緩存和數據庫。
  • 適用于數據量大,且 Key 的變化不頻繁的場景。

需要注意:

  • 布隆過濾器有一定的誤判率,因此不能完全避免緩存穿透。
  • 如果數據變化頻繁,需要定期更新布隆過濾器。

緩存空對象:簡單粗暴但有效

當數據庫查詢結果為空時,仍然將這個空結果緩存起來,下次再請求這個 Key 時,直接從緩存返回。

實現方式:

public Object getFromCache(String key) {     Object cacheValue = cache.get(key);     if (cacheValue == null) {         Object dbValue = getDataFromDB(key);         if (dbValue == null) {             // 緩存空對象             cache.put(key, new NullValue(), CACHE_EXPIRATION_TIME); // NullValue 是一個自定義的空對象         } else {             cache.put(key, dbValue, CACHE_EXPIRATION_TIME);             return dbValue;         }     }      if (cacheValue instanceof NullValue) {         return null; // 返回 null 表示數據不存在     }      return cacheValue; }  // 自定義空對象 class NullValue {}

優點:

  • 實現簡單,不需要引入額外的組件。
  • 能夠有效防止緩存穿透。

缺點:

  • 緩存中會存在大量的空對象,占用一定的存儲空間。
  • 如果空對象設置的過期時間較長,可能會導致數據不一致。

最佳實踐:

  • 空對象的過期時間不宜過長,可以設置一個較短的過期時間,例如 1 分鐘。
  • 可以考慮使用特殊的 Key 來標識空對象,例如 “NULL_” + key。

接口層校驗:亡羊補牢,防患于未然

在接口層對請求參數進行校驗,過濾掉不合法的 Key,避免請求打到緩存和數據庫。

實現方式:

  • 使用正則表達式校驗 Key 的格式。
  • 使用白名單或黑名單過濾 Key。
  • 對 Key 進行編碼或加密。

優點:

  • 能夠有效防止惡意攻擊。
  • 減輕緩存和數據庫的壓力。

缺點:

  • 需要對所有接口進行改造。
  • 可能會影響接口的性能。

需要注意:

  • 接口層校驗只能作為一種輔助手段,不能完全依賴接口層校驗來防止緩存穿透。

如何選擇合適的方案?

這取決于你的具體場景。

  • 數據量小,Key 的變化不頻繁: 可以考慮使用緩存空對象。
  • 數據量大,Key 的變化不頻繁: 可以考慮使用布隆過濾器。
  • 需要防止惡意攻擊: 可以考慮使用接口層校驗。
  • 多種方案結合使用: 可以將多種方案結合起來使用,以達到更好的效果。

緩存穿透和緩存擊穿、緩存雪崩有什么區別

緩存穿透、緩存擊穿和緩存雪崩是緩存使用中常見的三個問題,它們之間的區別如下:

  • 緩存穿透: 查詢一個數據庫中肯定不存在的數據。
  • 緩存擊穿: 緩存中某個熱點 Key 過期,導致大量請求打到數據庫。
  • 緩存雪崩: 緩存中大量的 Key 同時過期,導致大量請求打到數據庫。

簡而言之,緩存穿透是查不存在的數據,緩存擊穿是查存在的但過期的熱點數據,緩存雪崩是查大量同時過期的數據。

布隆過濾器如何選擇合適的哈希函數?

選擇合適的哈希函數對于布隆過濾器的性能至關重要。好的哈希函數應該滿足以下條件:

  • 均勻性: 哈希函數應該將元素均勻地分布到 bit 數組中,避免出現哈希沖突。
  • 獨立性: 不同的哈希函數應該相互獨立,減少誤判率。
  • 高效性: 哈希函數的計算速度應該足夠快,避免影響查詢性能。

常見的哈希函數包括:

  • MurmurHash: 一種非加密哈希函數,性能優異,廣泛應用于各種場景。
  • FNV Hash: 一種快速哈希函數,適用于對性能要求較高的場景。
  • MD5、SHA-1: 加密哈希函數,安全性高,但性能較差,不適合用于布隆過濾器。

在實際應用中,可以根據具體場景選擇合適的哈希函數。Guava 提供的 BloomFilter 默認使用 MurmurHash。

緩存空對象會帶來什么問題?如何避免?

緩存空對象的主要問題是:

  • 占用存儲空間: 緩存中會存在大量的空對象,占用一定的存儲空間。
  • 數據不一致: 如果空對象設置的過期時間較長,可能會導致數據不一致。

為了避免這些問題,可以采取以下措施:

  • 設置較短的過期時間: 空對象的過期時間不宜過長,可以設置一個較短的過期時間,例如 1 分鐘。
  • 使用特殊的 Key 標識空對象: 可以考慮使用特殊的 Key 來標識空對象,例如 “NULL_” + key。
  • 限制空對象的數量: 可以設置一個最大空對象數量,當空對象數量超過限制時,不再緩存新的空對象。
  • 定期清理空對象: 可以定期清理緩存中的空對象,釋放存儲空間。

? 版權聲明
THE END
喜歡就支持一下吧
點贊15 分享