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 {

    private int numHashFunctions;

    private int bitSize;

    private Funnel funnel;

    /**
     * 

* 1.构造函数判断Funnel是否为空,赋值。 * 2.计算bit的大小, * 3.需要哈希次数 *

* * @param funnel * @param expectedInsertions * @param fpp -》false positive */ public BloomFilterHelper(Funnel 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
     * @author Lifeifei
     * @date 2019/4/8 13:18
     */
    @Bean
    public BloomFilterHelper initBloomFilterHelper() {
        return new BloomFilterHelper<>((Funnel) (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  void addByBloomFilter(BloomFilterHelper 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  boolean includeByBloomFilter(BloomFilterHelper 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的过期。

 

你可能感兴趣的:(中间件,redis)