快取雪崩、快取擊穿、快取穿透是分散式系統中使用快取時,常遇到的三類問題,都會對系統效能和穩定性產生嚴重影響。下面將詳細介紹這三者的定義、產生原因、危害以及常見的解決方案。
1. 快取雪崩
1.1 定義
快取雪崩是指在某一時刻,大量快取同時失效,導致大量請求直接打到資料庫層,造成資料庫壓力驟增,甚至可能導致資料庫崩潰、系統不可用的情況。
1.2 產生原因
- 快取集中失效:通常情況下,快取的失效時間(TTL)是設定好的,但如果大量快取鍵設定了相同或接近的過期時間點,那麼在這些快取集中失效時,會造成大量的請求無法從快取中讀取資料,只能直接訪問資料庫。
- 快取伺服器當機:如果 Redis 伺服器叢集出現當機或故障,那麼所有快取資料會瞬間不可用,大量請求直接湧向資料庫。
1.3 危害
- 資料庫壓力激增:大量併發請求瞬間打到資料庫,可能造成資料庫連線數耗盡、效能下降,甚至當機。
- 服務不可用:由於資料庫無法及時響應請求,系統整體響應速度變慢或完全失去響應,導致服務不可用。
1.4 解決方案
-
快取過期時間分散化:
- 可以為不同的快取鍵設定不同的失效時間(TTL),使得快取的過期時間均勻分佈,避免大量快取同時失效。例如,在設定 TTL 時,加上一個隨機值,避免快取鍵在同一時間失效。
// 設定快取時,加一個隨機時間,防止集中過期 int randomTTL = ttl + new Random().nextInt(100); redisTemplate.opsForValue().set(key, value, randomTTL, TimeUnit.SECONDS);
-
快取預熱:
- 在系統上線前,提前將熱點資料載入到快取中,避免大量請求同時觸發快取未命中的情況。
-
降級策略:
- 在快取雪崩時,可以採取限流、降級等策略,減緩資料庫的壓力。如在快取失效時,直接返回預設值或快取過期的舊資料,避免資料庫短時間內處理大量請求。
-
多級快取架構:
- 使用本地快取(如 Caffeine、Guava 等)和分散式快取(如 Redis)相結合的方式,部分熱點資料可以先放入本地快取,降低 Redis 和資料庫的壓力。
-
Redis 高可用:
- 部署 Redis 主從叢集,使用 Redis 的哨兵模式(Sentinel)或者 Redis Cluster 來實現高可用,避免快取伺服器單點故障。
2. 快取擊穿
2.1 定義
快取擊穿是指快取中儲存的某個熱點資料在某一時刻失效,大量併發請求同時去訪問這個熱點資料,導致所有請求打到資料庫,造成資料庫壓力驟增的情況。
2.2 產生原因
- 熱點快取失效:當某個熱點資料的快取過期時,大量請求湧入到資料庫層,而此時資料庫需要處理所有的請求,造成資料庫的瞬時壓力增大。
2.3 危害
- 資料庫壓力過大:由於熱點資料失效,導致瞬間的大量請求直接打到資料庫,增加資料庫的壓力,可能會引發資料庫連線耗盡、響應變慢等問題,嚴重時可能導致資料庫當機。
2.4 解決方案
-
熱點資料永不過期:
- 對於特別重要的熱點資料,可以考慮不設定快取過期時間,讓這些資料一直儲存在快取中。可以透過定時任務手動更新快取中的資料來避免資料過期問題。
-
互斥鎖(Mutex)機制:
- 為了解決在快取失效瞬間,大量請求同時訪問資料庫的問題,可以透過加鎖機制,保證同一時刻只有一個執行緒能訪問資料庫。其他執行緒需要等待該執行緒將新資料寫入快取後,再讀取快取。
String value = redisTemplate.opsForValue().get(key); if (value == null) { // 獲取分散式鎖 if (redisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.SECONDS)) { try { // Double-check value = redisTemplate.opsForValue().get(key); if (value == null) { // 查詢資料庫 value = database.get(key); // 將結果寫入快取 redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS); } } finally { // 釋放鎖 redisTemplate.delete(lockKey); } } else { // 等待鎖釋放後,再從快取中讀取資料 Thread.sleep(100); // 自行調整等待時間 value = redisTemplate.opsForValue().get(key); } }
-
預防性快取更新:
- 在熱點資料即將過期時,提前非同步重新整理快取。透過檢測熱點資料的訪問頻率,當即將過期時觸發自動更新操作,避免過期瞬間的擊穿問題。
-
雙快取機制:
- 可以採用雙層快取策略:一個主要快取層負責快取大部分資料,另一個次快取層儲存上次的快取資料。在主要快取失效時,可以直接從次快取層讀取資料,避免直接打到資料庫。
3. 快取穿透
3.1 定義
快取穿透是指惡意使用者或程式請求查詢的資料在快取和資料庫中都不存在,導致每次請求都會直接打到資料庫,繞過快取。由於快取沒有儲存該請求的結果,所有這類請求都會繞過快取,直接訪問資料庫,從而導致資料庫承受巨大的壓力。
3.2 產生原因
- 惡意攻擊:有意構造大量不存在的資料請求,如查詢不存在的使用者 ID 或商品 ID,快取中沒有這些資料,因此直接請求資料庫。
- 查詢不存在的鍵:一些業務邏輯上無法避免查詢不存在的資料,例如使用者查詢某些過時或錯誤的請求引數,資料庫中也沒有相應的記錄。
3.3 危害
- 資料庫效能下降:由於查詢的資料既不在快取中,也不在資料庫中,因此每次請求都會直接打到資料庫,造成資料庫壓力增大,甚至引發效能瓶頸。
3.4 解決方案
-
快取空結果:
- 如果查詢的某個鍵在資料庫中不存在,則將該鍵的查詢結果(如
null
或空值)快取起來,並設定一個較短的過期時間,防止該鍵反覆查詢打到資料庫。
// 查詢快取 String value = redisTemplate.opsForValue().get(key); if (value == null) { // 查詢資料庫 value = database.get(key); if (value == null) { // 快取空結果,避免快取穿透 redisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES); } else { // 將資料庫中的值寫入快取 redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS); } }
- 如果查詢的某個鍵在資料庫中不存在,則將該鍵的查詢結果(如
-
布隆過濾器(Bloom Filter):
- 使用布隆過濾器對所有可能存在的資料進行標記,所有請求先經過布隆過濾器進行校驗,只有布隆過濾器認為存在的資料,才會去查詢快取或資料庫。這樣可以有效攔截掉絕大多數不存在的請求,防止這些請求繞過快取直接打到資料庫。
BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 100000); // 將所有可能的合法鍵加入布隆過濾器 bloomFilter.put("validKey1"); bloomFilter.put("validKey2"); // 查詢時先校驗布隆過濾器 if (!bloomFilter.mightContain(key)) { return "Invalid Key"; } // 正常查詢快取和資料庫
-
引數校驗:
- 在查詢請求進入系統前,進行嚴格的引數校驗和過濾,避免不合法的請求進入系統。例如使用者 ID 或商品 ID 是否符合格式要求,避免惡意構造的非法請求直接打到資料庫。