Redis學習篇-緩存穿透,緩存擊穿,緩存雪崩

前言:

隨着業務的發展,可能出現了大量數據的請求,在這個時候,如果所有的請求都湧入數據庫,就會造成數據庫的壓力增大,一些簡單的sql查詢因爲數據庫承受大量壓力的而幾何式變慢,甚至造成癱瘓。

因爲,爲了解決這個問題,引入了nosql,而redis則是nosql技術中的一種。

但是引入nosql,則會引入緩存穿透,緩存擊穿,緩存雪崩等問題,因此,本章則會關於這幾個問題,說說我自己的理解和解決。

本文所引用redis結構是一主二從三哨兵。

一.是什麼

既然引入redis會引入緩存穿透,緩存擊穿,緩存雪崩等,我們先來解釋一下這個幾個東西是什麼。

 緩存穿透:通過請求一個無論redis還是數據庫都不存在的key,因此請求訪問redis都是null,轉而請求數據庫,還是造成了大量請求同時間到達了數據庫,進而起到了壓垮了數據庫的作用。

緩存擊穿:對於某一個熱點key,不停地被高併發訪問,但是一旦該熱點key過期了,這時候成千上萬的請求立馬訪問到數據庫一層,起到壓垮數據庫的作用。

緩存雪崩:在某一個時間段,大量緩存集體過期,這個時候,由於緩存過期,所有的請求壓力轉移後端去,數據庫去。

二.怎麼解決

2.1緩存穿透解決辦法

面對緩存穿透比較常用的一種辦法就是使用布隆過濾器(Bloom Fliter),通過數據哈希存儲到一個巨大的bitmap中,從而避免進行到數據庫中。另外也有一種簡單的解決辦法就是把不存在key的空緩存也緩存到redis裏面去,設置好過期時間,最好不超過5分鐘。

根據網上找到布隆過濾器,根據guava裏面的布隆過濾器進行改造,方便分佈式使用。

public class BloomFilterHelper<T> {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    /**
     * <p>
     * 1.構造函數判斷Funnel是否爲空,賦值。
     * 2.計算bit的大小,
     * 3.需要哈希次數
     * </p>
     *
     * @param funnel
     * @param expectedInsertions
     * @param fpp -》false positive
     */
    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能爲空");
        this.funnel = funnel;
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        //計算hash次數
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }


    /**
     * 計算hashmap -使用murmur3-128
     * @param value
     * @return
     */
    public int[] murmurHashOffset(T value) {
        int[] offset = new int[numHashFunctions];

        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offset[i - 1] = nextHash % bitSize;
        }

        return offset;
    }

    /**
     * 計算bit數組長度
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            p = Double.MIN_VALUE;
        }
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    /**
     * 計算hash方法執行次數
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }
}

redis環境註冊

@Configuration
public class RedisConfig {
    @Autowired
    private RedisTemplate redisTemplate;

    @Bean
    public RedisTemplate redisTemplateInit() {
        //設置序列化Key的實例化對象
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //設置序列化Value的實例化對象
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    /**
     * <註冊BloomFilterHelper>
     *
     * @param
     * @return com.zy.crawler.config.redis.BloomFilterHelper<java.lang.String>
     * @author Lifeifei
     * @date 2019/4/8 13:18
     */
    @Bean
    public BloomFilterHelper<String> initBloomFilterHelper() {
        return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8)
                .putString(from, Charsets.UTF_8), 1000000, 0.01);
    }
}

RedisService

@Service
public class RedisService {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根據給定的布隆過濾器添加值
     */
    public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能爲空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /**
     * 根據給定的布隆過濾器判斷值是否存在
     */
    public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能爲空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }

        return true;
    }

}

redis裏面通過setBit進行存儲,因此即便某個key不存在,可以直接通過布隆過濾器查詢不同hash出的bit是否都存在。從而解決了問題。

2.2緩存擊穿解決方法

緩存擊穿一般是因爲某個熱點key過期後,成千上萬的請求突然轉向數據庫增大數據庫的壓力。因此一般的做法的加個互斥鎖(mutex).一旦某個熱點過期了,設置互斥鎖,第一個獲取鎖的重新設置熱點值,後面的請求則休眠一秒,等待第一個請求設置了熱點值後,重新獲取熱點值。

這是在serviceimpl的方法

@Override
    public String getTalkingPoint(String key) {
        String talkingPointValue = (String)redisTemplate.opsForValue().get(key);
        if(talkingPointValue ==null){
            //互斥鎖
            if(redisTemplate.opsForValue().setIfAbsent(key+"_mutex","key_mutex",3,TimeUnit.MINUTES)){
                //從數據庫查詢
                HighTecoEntity byId = this.getById(1);
                redisTemplate.opsForValue().set(key,byId.getUser(),3,TimeUnit.MINUTES);
                return byId.getUser();
            }else{
                //拿不到互斥鎖,休眠一秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.info("獲取失敗");
                    return null;
                }
                String getTalkingValueByRedis = (String) redisTemplate.opsForValue().get(key);
                return null;
            }
        }
        return talkingPointValue;
    }

2.3緩存雪崩

緩存雪崩相對緩存擊穿而言,是單個key值過期和N個key值同時過期,這時短時間的大量數據讀寫操作極大可能導致數據庫垮掉。我們可以選擇通過隨機因子把不同類型的key分散開來,除此之外,還可以增加其過期時間,即原本是30min,後面可以設置成60min,防止 大規模key的過期。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章