redis——缓存雪崩、缓存穿透、缓存击穿

通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

缓存雪崩

如果同一时间大量的缓存数据同时过期或redis服务宕机,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机。
在这里插入图片描述发生缓存雪崩有两个原因

  • 大量的缓存数据同时过期
  • redis服务宕机

大量的缓存数据同时过期的解决方案

1、给不同的key随机设置过期时间 (TTL)
2、互斥锁

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

3、双 key 策略

我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。

当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。

4、多级缓存

jvm本地缓存,nginx缓存等

Redis 故障宕机引发的缓存雪崩的解决方案

1、给缓存业务添加服务熔断或请求降级限流策略 (sentinel);
2、搭建redis集群,提供redis服务的可用性;

缓存击穿

缓存击穿也称(热点key问题),一个被高并发访问缓存业务重建困难的key突然过期(失效)了,于是全部请求都直接访问数据库,从而导致数据库的压力骤增甚至宕机。
如:tb秒杀活动,wb热榜等

在这里插入图片描述

解决方案

1、互斥锁

在这里插入图片描述
互斥锁保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。最后给锁也添加过期时间

redis命令

SETNX KEY_NAME VALUE

使用setIfAbsent,springboot2.1版本以上才支持

stringRedisTemplate.opsForValue().setIfAbsent(key, "1",10, TimeUnit.SECONDS);

使用lua脚本

private Boolean tryGetLock1(String key){

        /** redisUtil.setIfAbsent 新加的带有超时的setIfAbsent 脚本*/
        //KEYS[1] 用来表示在redis 中用作键值的参数占位,
        // 主要用來传递在redis 中用作keys值的参数

        // ARGV[1] 用来表示在redis 中用作参数的占位,
        // 主要用来传递在redis中用做 value值的参数。
        String newSetIfAbsentScriptStr = " if 1 == redis.call('setnx',KEYS[1],ARGV[1]) then" +
                " return 1;" +
                " else" +
                " return 0;" +
                " end;";
        //创建 redis脚本对象
        RedisScript<Boolean> newSetIfAbsentScript = new DefaultRedisScript<>(newSetIfAbsentScriptStr,Boolean.class);
        List<String> keys = new ArrayList<>();
        keys.add(key);  // key
        Object[] values = {"1"};  // value
        // 执行脚本
        Boolean res = stringRedisTemplate.execute(newSetIfAbsentScript, keys, values);
        System.out.println("result:"+res);
        return res;

    }

上述的分布式锁存在的问题:1、不可重入;2、不可重试;3、超时释放(如果业务还没执行完,这样就会出问题)

在工作中建议使用redisson的lock、tryLock等

redisson可重入锁基本使用及原理分析

2、逻辑过期

流程图
在这里插入图片描述

时序图
在这里插入图片描述
逻辑过期是指不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间

逻辑过期代码实现示例

 /**
     * 逻辑过期解决缓存击穿
     */
    private Shop queryWithLogicalExpire(Long id){
        String key = CACHE_SHOP_KEY + id;
        // 查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 缓存命中,先将json反序列化成对象
        RedisData  data = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject jsonData = (JSONObject) data.getData();
        Shop notExpireShop = JSONUtil.toBean(jsonData, Shop.class);
        LocalDateTime expireTime = data.getExpireTime();
        // 过期时间在当前时间之后,数据未过期,直接返回
        if (expireTime.isAfter(LocalDateTime.now())){
            return notExpireShop;
        }

        // 已过期,缓存重建
        String lockKey = "lock:shop:"+id;
//        //获得互斥锁
//        Boolean lock = setIfAbsent(lockKey);
        RLock rLock = redissonClient.getLock(lockKey);
        boolean lock = rLock.tryLock();
        //判断是否成功获得锁
        if(lock){
            //获取锁成功,开启独立线程,缓存重建
            es.submit(()->{
                try {
                    //缓存重建
                    this.saveShopToRedis(id, 20L);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        //锁获取失败,返回旧的商铺信息
        return notExpireShop;

    }

互斥锁和逻辑过期对比
在这里插入图片描述

缓存穿透

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,于是全部请求都直接访问数据库,从而导致数据库的压力骤增甚至宕机。
在这里插入图片描述
存在原因:恶意攻击,故意大量访问某些读取不存在数据的业务

解决方案

  • 请求校验
  • 缓存空值
  • 增加id的复杂度,避免被猜到id的规律
  • 做好热点数据的限流
  • 加强用户权限的校验
  • 布隆过滤器

布隆过滤器校验的结果特点是:

  • 若过滤器判断某个元素存在,那么这个元素不一定存在
  • 若过滤器判断某个元素不存在,那么这个元素一定不存在
    布隆过滤器讲解:https://blog.csdn.net/cssweb_sh/article/details/124284785

布隆过滤器代码实现示例

  /**
     * 初始化布隆过滤器
     */
	@PostConstruct
    public void initBloomFilter(){

        List<Long> ids = list().stream()
                            .map(Shop::getId)
                            .collect(Collectors.toList());
//        // 设置布隆过滤器的误判率
//        bloomFilter.tryInit(ids.size(),0.01);
        // 将所有店铺信息添加到布隆过滤器
        bloomFilter.add(ids);
    }

/**
     * 布隆过滤器解决缓存穿透
     */
    public Shop queryWithPassThroughByBf(Long id){
        boolean b = bloomFilter.contains(id);
        String key = CACHE_SHOP_KEY + id;
        if (b){
            // 查询缓存
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopJson)){
                Shop shop = JSONUtil.toBean(shopJson, Shop.class);
                return shop;
            }
            // ""空值不是null
            if (shopJson != null){
                return null;
            }
            // 查询数据库
            Shop shop = getById(id);
            if (shop == null){
                // 若数据不存在,写入空值来应对缓存穿透
                stringRedisTemplate.opsForValue().set(key, "",2, TimeUnit.MINUTES);
                return null;
            }
            // 保存缓存到redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),10, TimeUnit.MINUTES);
            return shop;
        }
        return null;
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值