天天看点

Redis的缓存穿透,雪崩和击穿问题以及分别的解决方案

Redis的缓存穿透,雪崩,和击穿问题以及分别的解决方案

    • 缓存穿透
    • 缓存雪崩
    • 缓存击穿
    • 解决缓存击穿问题
      • 分布式锁主流的解决方案有三种:
        • 基于数据库实现分布式锁
        • 基于Zookeeper
        • 基于缓存(redis)
          • 使用redisson解决分布式锁问题
      • 使用布隆过滤器结果缓存穿透问题

缓存穿透

缓存穿透:查询一个缓存中不存在的数据这个时候就会去查询数据库,但是数据库中也没有这个数据,这个时候基于容错考虑,不存储null到redis缓存中区,然后导致每次都查询数据库,当流量大的时候,出现宕机的问题。

解决 null空结果也进缓存但给让他设置一个短一点的超时时间,不超过5分钟,或者使用布隆过滤器解决。

缓存雪崩

缓存雪崩:当我们在给缓存中数据设置了相同的过期时间的时候,导致在某一时刻缓存全部失效,然后将压力都给了DB,数据库的压力过大导致雪崩。

解决:在原来的缓存过期时间的基础上,加一个随机数,这样就可以避免缓存同时过期。

缓存击穿

缓存击穿:对于热点key,当我们设置的过期时间刚好结束的同时大量的请求进来,这个时候就会全部去数据库中去找数据,我们就称为缓存击穿。

解决方案:(唯一一个在查询的时候需要加锁的情况就是缓存击穿,加锁是为了保护数据库)在分布式的环境下我们使用分布式锁来解决,比如redis中的setnx,zookeeper中的临时顺序节点或者mysql来实现。

分布式事务就是解决缓存击穿问题的。

解决缓存击穿问题

分布式锁主流的解决方案有三种:

基于数据库实现分布式锁

加锁就是给数据库表添加数据,通过数据库中的唯一索引实现,将唯一索引这一列设为方法,当相同的方法名进来的时候只有现在存在的这个方法销毁后,它才能将后来的相同的方法名的数据添加进来

基于Zookeeper

zookeeper是一个分布式文件管理系统,在我们的持久化节点下面有多个节点,每个节点是有顺序的只有我们的第一个节点可以拿到锁,二节点监督一节点,三节点监督二节点。谁排在第一位谁获取锁,当第一个节点释放锁,删除节点后,下一个节点上位,拿到锁。

基于缓存(redis)

占坑思想 setnx 在设置key的时候,只能存在一个key,才能设置成功。获取锁的本质其实就是使用setnx添加一个数据,一个并发的线程进来先在方法的最开始使用setnx先获取锁,如果获取到了就继续执行操作,如果没有获取到就睡眠一定的时间后自旋继续访问。一定要注意的是在执行结束之后一定要释放锁,也就是将key删除。

//获取锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock")
        if (flag){
            String num =  redisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(num)){
                return;
            }
            int i = Integer.parseInt(num);
            redisTemplate.opsForValue().set("num",String.valueOf(++i));
            //删除锁
            redisTemplate.delete("lock");
        }else{
            try {
            //当没有拿到锁的时候先睡眠后自旋
                Thread.sleep(100);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
           

解决拿到锁的服务器突然宕机导致的死锁问题 :为了避免拿到锁的的服务器突然宕机这个时候其他线程造成死锁,我们在拿到锁的同时直接给他设置超时时间。

//在获取锁的同时设置超时时间,保证了它两的原子性
  Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock",3, TimeUnit.MICROSECONDS);
           

解决防止误删操作 当前线程超时导致后面的线程进入,当前线程删除后进入线程的锁,导致无锁现象的出现(问题的描述就是,当我们设置了超时时间后,我们的在拿到锁之后但是程序没有执行结束的时候锁被因为超时自动释放掉了,这个时候虽然锁释放掉了但是线程还在继续执行,同时另一个线程进来拿到了锁,但刚好在这是上一个线程执行结束将刚刚才拿到lock锁的这个线程的锁直接删掉了)

当前lock锁的value使用uuid的随机数 然后使用Lua脚本和并且创建脚本对象发送指令区当时同一个线程的锁的时候删除当前锁

String uuId = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuId, 3, TimeUnit.SECONDS);
        //判断
        if(flag){
            //读取数据
            String num = redisTemplate.opsForValue().get("num");
            //转换
            if(StringUtils.isEmpty(num)){
                return ;
            }
            int number = Integer.parseInt(num);
            //自增覆盖
            redisTemplate.opsForValue().set("num",String.valueOf(++number));
             //lua脚本
            String script="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            //创建脚本对象
            DefaultRedisScript<Long> redisScript=new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);
            //发送指令
            redisTemplate.execute(redisScript, Arrays.asList("lock"),uuId);
//            if(uuId.equals(redisTemplate.opsForValue().get("lock"))){
//                //释放锁
//                this.redisTemplate.delete("lock");
//            }
        }else {
            try {
                Thread.sleep(100);
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

           
使用redisson解决分布式锁问题

(Redisson是一个在Redis的基础上实现的Java驻内存数据网格)

使用之前必须导入启动器依赖和配置

配置类:

@Data                             //利用getter和setter方法进行自动读取配置类中对应的几个属性的值
@Configuration
@ConfigurationProperties("spring.redis")  //这里读取的是那个服务依赖这个这个模块就读取那个模块的配置文件
public class RedissonConfig {

    private String host;

    private String password;

    private String port;

    private int timeout = 3000;
    private static String ADDRESS_PREFIX = "redis://";

    /**
     * 自动装配
     */
    @Bean
    RedissonClient redissonSingle() {
        Config config = new Config();

        if(StringUtils.isEmpty(host)){
            throw new RuntimeException("host is  empty");
        }
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
                .setTimeout(this.timeout);
        if(!StringUtils.isEmpty(this.password)) {
            serverConfig.setPassword(this.password);
        }
        return Redisson.create(config);
    }
}
           

使用:

@SneakyThrows
    public  void testLock() {
        RLock lock = redissonClient.getLock("lock");
        //lock.lock(3,TimeUnit.SECONDS);
        boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
        if (flag){
            String num = redisTemplate.opsForValue().get("num");
            //转换
            if(StringUtils.isEmpty(num)){
                return ;
            }
            int number = Integer.parseInt(num);
            //自增覆盖
            redisTemplate.opsForValue().set("num",String.valueOf(++number));
            lock.unlock();
        }else{
            Thread.sleep(100);
            this.testLock();
        }


    }
           

redis的性能最高

zookeeper的可靠性最高,因为他是强一致性

使用布隆过滤器结果缓存穿透问题

布隆过滤器:

Redis的缓存穿透,雪崩和击穿问题以及分别的解决方案

它的插入过程就是:首先得来了解误判率,误判率设置的越小,需要计算这个值得哈希算法越多,占用的二进制的空间越大。根据不同的哈希算法算出来的值对应的二进制数据的值就改为1

删除的过程:删除也是根据同样的哈希算法将对应的二进制数据的值进行判断是不是都是1如果都是就删除,但是可能存在哈希碰撞问题不同的值有相同的哈希算法算出来的值相同,想要避免这样的哈希碰撞问题,我们就可以将误判率设置的小一点,使得计算的哈希算法就越多,避免哈希碰撞。

查询,一个值进来根据哈希算法进行计算将算出来的值对应的二进制数据对应的下标中的数据是否为1,如果所用的哈希算法计算出来的都能对应上则说明存在这个值。

布隆过滤器的使用:

首先需要在我们的启动类中去初始化我们的布隆过滤器

public class ServiceProductApplication implements CommandLineRunner {   //首先需要实现CommandLineRunner接口
@Override
public void run(String... args) throws Exception {
    //获取到布隆过滤器
    RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
    //初始化
    bloomFilter.tryInit(10000,0.01);
}
           

其次在我们添加sku的时候将skuid存入到我们的布隆过滤器中

//在我们创建完sku进行保存的时候我们将sku对应的id到布隆过滤器中
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
//首先拿到布隆过滤器,其次将skuinfo的id保存到布隆过滤器中
bloomFilter.add(skuInfo.getId());
           

最后当我们通过skuid查询商品你详情页的时候我们先去判断布隆过滤器中是否存在这个skuid 从而做到了避免无效数据进入数据库导致缓存击穿问题

HashMap<String, Object> map = new HashMap<>();
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
boolean contains = bloomFilter.contains(skuId);
if (!contains){
    return map;
}
           

继续阅读