高併發下快取失效問題-快取穿透,快取擊穿,快取雪崩
阿新 • • 發佈:2022-05-29
1.快取穿透
快取穿透是指:
- 大量併發訪問一個不存在的資料,先去看快取中,發現快取中不存在,所以就去資料庫中查詢,但是資料庫中也不存在並且並沒有把資料庫中這個不存在的資料null放入快取,導致所有查詢這個不存在的請求全部壓到了資料庫上,失去了快取的意義.請求特別大就會導致資料庫崩掉
風險:
- 利用不存在的資料進行攻擊,資料庫瞬時壓力增大,最終導致崩潰
- 隨機key,大量攻擊(預防);隨機值穿透攻擊
解決辦法:
- 快取null值:
- 針對不存在的資料,我們將null快取並且加入短暫的過期時間
- 布隆過濾器:
- 針對隨機key穿透,我們可以使用布隆過濾器
布隆過濾器
布隆過濾器資料一致性
執行流程
2.快取擊穿
快取擊穿是指:
- 大量併發查詢一個熱點資料,但是呢我們的熱點資料在某一刻剛好過期了,這樣大量的併發請求會先經過快取,但是快取中沒有,再進入布隆過濾器bloom儲存了該熱點資料的ID所以會讓請求去查詢資料庫,結果這大量請求就把資料庫壓垮了
風險:
- 由於快取某一刻會過期,剛好該時刻大量併發出來,資料庫瞬時壓力增大,最終導致崩潰
解決辦法:
- 加鎖:
- 本地鎖: 直接使用synchronize,juc.lock不適用於分散式情況,分散式下他們只能鎖住當前自己的服務
- 分散式鎖:
分散式鎖階段演進
-
加鎖,就是"搶坑位"
-
第一階段
-
第二階段
-
第三階段
-
第四階段
-
第五階段
-
Redis原生實現分散式鎖核心程式碼如下:
/** * 根據skuId查詢商品詳情 * * 使用Redis實現分散式鎖: * 解決大併發下,快取擊穿|穿透問題 * * @param skuId * @return */ @Override public SkuItemTo findSkuItem(Long skuId) { // 快取key String cacheKey = RedisConstants.SKU_CACHE_KEY_PREFIX + skuId; // 查詢快取 SkuItemTo data = cacheService.getData(RedisConstants.SKU_CACHE_KEY_PREFIX + skuId, new TypeReference<SkuItemTo>() { }); // 判斷是否命中快取 if (data == null) { // 快取沒有,回源查詢資料庫.但是這個操作之前先問一下bloom是否需要回源 if (skuIdBloom.contains(skuId)) { // bloom返回true說明資料庫中有 log.info("快取沒有,bloom說有,回源"); SkuItemTo skuItemTo = null; // 使用UUID作為鎖的值,防止修改別人的鎖 String value = UUID.randomUUID().toString(); // 摒棄setnx ,加鎖個設定過期時間不是原子的 // 原子加鎖,防止被擊穿 分散式鎖 設定過期時間 Boolean ifAbsent = stringRedisTemplate.opsForValue() .setIfAbsent(RedisConstants.LOCK, value, RedisConstants.LOCK_TIMEOUT, TimeUnit.SECONDS); if (ifAbsent) { try { // 設定自動過期時間,非原子的,加鎖和設定過期時間不是原子的操作,所以會出現問題 // stringRedisTemplate.expire(RedisConstants.LOCK, RedisConstants.LOCK_TIMEOUT, TimeUnit.SECONDS); // 大量請求,只有一個搶到鎖 log.info(Thread.currentThread().getName() + "搶到鎖,查詢資料庫"); skuItemTo = findSkuItemDb(skuId); // 執行回源查詢資料庫 // 把資料庫中查詢的資料快取裡存一份 cacheService.saveData(cacheKey, skuItemTo); } finally { // 解鎖前有可能出現各種問題導致解鎖失敗,從而出現死鎖 // 釋放鎖,非原子,不推薦使用 // String myLock = stringRedisTemplate.opsForValue().get(RedisConstants.LOCK); //刪鎖: 【對比鎖值+刪除(合起來保證原子性)】 String deleteScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Long executeResult = stringRedisTemplate.execute(new DefaultRedisScript<Long>(deleteScript,Long.class), Arrays.asList(RedisConstants.LOCK), value); // 判斷是否解鎖成功 if (executeResult.longValue() == 1) { log.info("自己的鎖:{},解鎖成功", value); stringRedisTemplate.delete(RedisConstants.LOCK); } else { log.info("別人的鎖,解不了"); } } } else { // 搶鎖失敗,自旋搶鎖. 但是實際業務為我們只需要讓讓程式緩一秒再去查快取就好了 try { log.info("搶鎖失敗,1秒後去查詢快取"); Thread.sleep(1000); data = cacheService.getData(RedisConstants.SKU_CACHE_KEY_PREFIX + skuId, new TypeReference<SkuItemTo>() { }); return data; } catch (InterruptedException e) { } } return skuItemTo; } else { log.info("快取沒有,bloom也說沒有,直接打回"); return data; } } log.info("快取中有資料,直接返回,不回源"); // 價格不快取,有些需要變的資料,可以"現用現拿" Result<BigDecimal> decimalResult = productFeignClient.findPriceBySkuId(skuId); if (decimalResult.isOk()) { BigDecimal price = decimalResult.getData(); data.setPrice(price); } return data; }
- Redisson框架實現分散式鎖
3.快取雪崩
快取雪崩是指:
- 大量key同時過期,正好百萬請求進來,全部要查這些資料?一查資料庫就炸了
解決辦法:
- 過期時間+隨機值防止大面積同時失效; 單點失效,自然會由防擊穿來加鎖處理
@Override
public void saveData(String key, Object data) {
if (data == null) {
// 快取null值,防止快取穿透.設定快取過期時間
stringRedisTemplate.opsForValue().set(key, cacheConfig.getNullValueKey(), cacheConfig.getNullValueTimeout(), cacheConfig.getNullTimeUnit());
} else {
// 為了防止快取同時過期,發生快取雪崩.給每個快取過期時間加上隨機值
Double value = Math.random() * 10000000L;
long mill = 1000 * 60 * 24 * 3 + value.intValue();
stringRedisTemplate.opsForValue().set(key, JsonUtils.objectToJson(data),
mill, cacheConfig.getDataTimeUnit());
}
}