Redis非关系型数据库缓存穿透、击穿与缓存雪崩

缓存穿透

缓存系统,一般流程都是按照key去查询缓存,如果不存在对应的value,就去后端系统(例如:持久层数据库)查找。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力,这就叫做缓存穿透。

正常请求:
在这里插入图片描述
缓存击穿时:
在这里插入图片描述

如何避免

1. 缓存空结果

对查询结果为空的情况进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2. 布隆过滤器
采用布隆过滤器,guava有实现api,或者使用redis的bitmap。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。布隆过滤器对于固定的数据可以起到很好的效果,但是对于频繁更新的数据,布隆过滤器的构建会面临很多问题。另外布隆过滤器是有判断误差的,网上有很多详细的介绍,请读者自行搜索即可。
在这里插入图片描述

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

如何避免

1. 互斥锁
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

在这里插入图片描述

如果是单机,可以用synchronized或者lock来处理,如果是分布式环境就需要使用分布式锁。
使用互斥锁,代码如下,仅适用redis2.6.1以后支持setnx的版本。在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用redis的setnx操作去set一个mutex key。当操作返回成功时,再进行load db的操作并回设缓存,否则,就重试整个get缓存的方法。

在这里插入图片描述


public String get(key) {
      List<String> resultList = (List<String>)redisTemplate.opsForValue().get(key);
    if(CollectionUtils.isEmpty(resultList)){
        final String mutexKey = key + "_lock";
        boolean isLock = (Boolean) redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                //只在键key不存在的情况下,将键key的值设置为value,若键key已经存在,则 SETNX 命令不做任何动作
                //命令在设置成功时返回 1 , 设置失败时返回 0
                return connection.setNX(mutexKey.getBytes(),"1".getBytes());
            }
        });
        if(isLock){
            //设置成1秒过期
            redisTemplate.expire(mutexKey, 1000, TimeUnit.MILLISECONDS);
            resultList = getValueBySql(key);
            redisTemplate.opsForValue().set(key, resultList, 1000, TimeUnit.SECONDS);
            redisTemplate.delete(mutexKey);
        }else{
            //线程休息50毫秒后重试
            Thread.sleep(50);
            retryCount--;
            System.out.println("=====进行重试,当前次数:" + retryCount);
            if(retryCount == 0){
                System.out.println("====这里发邮件或者记录下获取不到数据的日志,并为key设置一个空置防止重复获取");
                List<String> list = Lists.newArrayList("no find");
                redisTemplate.opsForValue().set(key, list, 1000, TimeUnit.SECONDS);
                return list;
            }
            return getCacheSave2(key,retryCount);
        }
    }
    return resultList;
}

2. 设置随机过期时间

不同的key,设置不同的过期时间,让缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低。

3. 设置二级缓存

做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

4. “永远不过期”

“永远不过期”包含两层意思:

从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

从功能上看,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。

在这里插入图片描述
这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

缓存预热

有效应对缓存的击穿和雪崩的一种方式是缓存预热。 缓存预热就是系统上线前,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。

解决思路

  1. 直接写个缓存刷新页面,上线时手工操作下。
  2. 数据量不大,可以在项目启动的时候自动进行加载。
  3. 定时刷新缓存。

限流

有效应对缓存的击穿和雪崩的另一种方式是限流。

在缓存失效后,通过队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

常见的限流算法

  1. 固定时间窗口算法(计数器)
  2. 滑动时间窗口算法
  3. 令牌桶算法
  4. 漏桶算法

总结

缓存穿透、击穿和雪崩是以预防为主、补救为辅,而在应对缓存的问题其实也没有一个完全完美的方案,只有最适合自己业务系统的方案。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaonannet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值