緩存穿透是指查詢一個數據庫中肯定不存在的數據,導致每次請求都打到數據庫,解決方案有:1. 使用布隆過濾器,通過bit數組和哈希函數高效判斷key是否存在,但有一定誤判率;2. 緩存空對象,在數據庫無數據時緩存空對象以減少后續請求;3. 接口層校驗,對請求參數進行合法性校驗,防止非法請求到達數據庫。
緩存穿透,簡單來說,就是查詢一個數據庫里肯定不存在的數據。由于緩存中沒有,每次請求都會打到數據庫,如果這種請求量很大,數據庫就可能扛不住。
解決方案:
布隆過濾器:高效判斷 Key 是否存在
布隆過濾器是一種概率型數據結構,能告訴你“可能存在”或“肯定不存在”。 它的優點是空間效率和查詢效率都很高,缺點是有一定的誤判率(false positive)。
立即學習“Java免費學習筆記(深入)”;
工作原理:
- 初始化: 創建一個 bit 數組,所有位初始化為 0。定義 k 個不同的哈希函數。
- 添加元素: 將元素通過 k 個哈希函數計算出 k 個哈希值,然后將 bit 數組中對應的 k 個位置置為 1。
- 查詢元素: 將元素通過 k 個哈希函數計算出 k 個哈希值,檢查 bit 數組中對應的 k 個位置是否都為 1。如果都為 1,則“可能存在”,否則“肯定不存在”。
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。
- 限制空對象的數量: 可以設置一個最大空對象數量,當空對象數量超過限制時,不再緩存新的空對象。
- 定期清理空對象: 可以定期清理緩存中的空對象,釋放存儲空間。