缓存穿透、雪崩、击穿常见解决方案

一、缓存穿透

1.什么是缓存穿透

缓存穿透是指在用户发送请求的时候,在缓存中根本不存在缓存数据,从而直接访问数据库,如果在高并发的情况下,很多请求直接访问数据库,会给数据库带来很大的压力,从而导致性能降低

2.解决办法

1. 设置空对象

在第一次查询数据库的时候没有查到就直接在缓存中写入空值,下次再查询的时候就会在缓存中进行查询了,可以较为有效的解决缓存穿透的问题

//  从redis中查询缓存 
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //  判断商品是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //  存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 如果为空值返回错误信息
        if (shopJson != null) {
            return null;
        }
        //  不存在,根据id查询数据库
        Shop shop = getById(id);
        //  不存在,返回错误
        // 这里判断shop为空 就说明数据库中也没有数据,这里我们就要设置一个空对象
        // 避免下次用户访问的时候再次访问数据库,这样可以直接访问缓存,虽然是空的
        // 但是避免了直接访问数据库的情况
        if (shop == null) {
            // 写入空值 --解决缓存穿透
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //  存在,写入缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //  返回
        return shop;

2.使用布隆过滤器

布隆过滤器相当于一个算法,在请求到达redis之前再进行一次判断查找的数据是否存在

        int expectedInsertions = 1000000;
        double falsePositiveRate = 0.01;

        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), expectedInsertions, falsePositiveRate);

        // 添加数据到布隆过滤器
        bloomFilter.put("数据1");
        bloomFilter.put("数据2");

        // 查询数据是否存在于布隆过滤器中
        System.out.println(bloomFilter.mightContain("数据1")); 
        System.out.println(bloomFilter.mightContain("数据2"));

二、缓存雪崩

1.什么是缓存雪崩

缓存雪崩指的是redis中缓存的所有的key都失效或者是redis服务器宕机了,而造成所有的请求直接到达数据库,这样会给数据库造成较大的压力

2.解决办法

  1. 给不同的key的TTL添加随机值
  2. 利用redis集群进行处理,redis哨兵模式可以在redis服务器宕机时合理的分配集群中的其它服务器进行处理
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存:可以不仅仅在redis中添加缓存,还可以在nginx中添加缓存,还可以在JVM中添加本地缓存,还可以在tomcat堆内缓存等等

感兴趣的可以尝试一下

三、缓存击穿

1.什么是缓存击穿

缓存击穿问题也叫做热点key问题,就是一个被高并发访问并且重建业务逻辑比较复杂的key突然失效了,高并发的访问会给数据库带来较大的压力

2.解决方法

1.采用互斥锁的方法进行处理(自定义互斥锁)

采用互斥锁的优点:

  1. 没有额外的内存消耗,保证一致性
  2. 实现简单

缺点:

  1. 线程需要等待,性能受到影响
  2. 可能有死锁的风险
    // 首先需要定义互斥锁
    // 自定义互斥锁
    private boolean mutexLock(String key) {
        // 利用redis的setnx进行互斥锁的实现
        String lockKey = LOCK_SHOP_KEY + key;
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    
    // 最后需要释放锁---不然其他线程一直在等待,就可能出现死锁的可能
    // 删除互斥锁
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

这里什么是redis的setnx:redis的setnx是命令语句,这里指的是只有存在的当key不存在时才能编辑它的value,如果存在的话再去编辑这个key的value它的值是不会发生改变的

 //  从redis中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //  判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //  存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 如果为空值返回错误信息
        if (shopJson != null) {
            return null;
        }
        // 4.实现缓存重建
        // 4.1获取互斥锁
        // 这里线程休眠是会抛异常的,所以用try-catch,而且最终结果都会释放锁(finally)
        Shop shop = null;
        try {
            boolean isLock = mutexLock(LOCK_SHOP_KEY + id);
            // 4.2判断是否获取成功
            if (!isLock) {
                // 4.3失败,则休眠等待
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            // 4.4成功,根据id查询数据库
            shop = getById(id);
            // 模拟重建缓存延时
            Thread.sleep(200);
            //  不存在,返回错误
            if (shop == null) {
                // 写入空值 --解决缓存穿透
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //  存在,写入缓存
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放互斥锁
            unLock(LOCK_SHOP_KEY + id);
        }
        //  返回
        return shop;

2.逻辑过期时间的方法进行处理(新添加一个逻辑过期的字段)

采用逻辑过期时间的处理优点:

      线程无需等待,性能较好

缺点:

  1. 不保证一致性
  2. 有额外的内存消耗
  3. 实现复杂
// 首先创建一个redisData类 
// 这里使用组合的方法来实现,当然还可以使用继承的方法来实现
// 使用组合的方法可以使耦合度没那么高。更加灵活
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;//这里的Object相当于泛型的作用,可以支持多种类型的数据
}
    // 将信息加入缓存当中----缓存预热 -- 缓存重建
    public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
        //查询店铺数据
        Shop shop = getById(id);
        // 模拟延迟时间
        Thread.sleep(200);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
// 创建线程池 --- 线程池大小为10
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

//这里创建一个线程池是为了等会缓存重建的时候能够单独开启一个线程去进行缓存的重建
 //  从redis中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //  判断缓存是否存在
        if (StrUtil.isBlank(shopJson)) {
            //  不存在,直接返回
            return null;
        }
        // 命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 判断是否过期
        // expireTime.isAfter是判断expireTime是否在现在的时间之后
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 未过期,直接返回信息
            return shop;
        }
        // 已过期, 需要重建缓存
        // 缓存重建
        // 获取互斥锁
        boolean isLock = mutexLock(CACHE_SHOP_KEY + id);
        // 判断锁是否获取成功 --- 获取成功后还要dbcheck,判断redis缓存是否过期
        if (isLock) {
        // expireTime.isBefore是判断expireTime是否在现在的时间之前
//            if (expireTime.isBefore(LocalDateTime.now())) {
            // 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShopToRedis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(CACHE_SHOP_KEY + id);
                }
            });
        }
//        }
        // 返回过期的商铺信息
        return shop;

总结

缓存穿透,击穿都是高并发中比较常见的问题,在处理这类问题要判断问题是什么,然后该怎么做,用什么方法做,最主要是要逻辑清晰的知道该做什么

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
对于 Redis 缓存的穿透、击穿雪崩问题,可以采取以下解决方案: 1. 缓存穿透:当请求的数据在缓存中不存在时,会直接访问数据库,如果有恶意攻击者大量请求不存在的数据,会给数据库造成很大压力。解决方案可以是在查询前进行参数校验,比如对请求的参数进行合法性检查,或者使用布隆过滤器等技术来快速判断请求的数据是否存在。 2. 缓存击穿:当某个热点数据过期或被删除时,大量请求同时涌入,导致请求直接访问数据库。为了解决这个问题,可以使用互斥锁(Mutex)或者分布式锁来避免多个请求同时访问数据库。在获取锁之前,首先尝试从缓存获取数据,如果缓存中存在,则直接返回;如果不存在,则获取锁,并从数据库中获取数据并放入缓存,最后释放锁。 3. 缓存雪崩:当缓存中的大量数据同时过期时,会导致大量请求直接访问数据库,给数据库带来巨大压力。为了解决这个问题,可以采取以下措施: - 设置合理的缓存过期时间,使得不同数据的过期时间错开,避免同时失效。 - 使用热点数据预加载,提前将热点数据加载到缓存中,避免同时失效。 - 使用多级缓存架构,将缓存分为多个层级,不同层级的缓存设置不同的过期时间,从而降低缓存失效的风险。 - 引入限流和熔断机制,对请求进行限制,避免大量请求同时访问数据库。 通过以上措施,可以有效地解决 Redis 缓存穿透击穿雪崩问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值