1. 程式人生 > 其它 >高併發下快取失效問題-快取穿透,快取擊穿,快取雪崩

高併發下快取失效問題-快取穿透,快取擊穿,快取雪崩

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());
        }
    }