44

快速了解缓存穿透与缓存雪崩

 4 years ago
source link: https://www.tuicool.com/articles/BNvURj3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

缓存穿透

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

正常请求:

zEV7Vnu.png!web

缓存击穿时:

7reyQvF.png!web

如何避免

1. 缓存空结果

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

2. 布隆过滤器

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

6RJNVrF.png!web

缓存雪崩

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

如何避免

1. 互斥锁

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

3Av6ruN.png!web

如果是单机,可以用synchronized或者lock来处理,如果是分布式环境就需要使用分布式锁。

使用互斥锁,代码如下,仅适用redis2.6.1以后支持setnx的版本。 在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用redis的setnx操作去set一个mutex key。

当操作返回成功时,再进行load db的操作并回设缓存,否则,就重试整个get缓存的方法。

iAz6b2J.jpg!web

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. “永远不过期”

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

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

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

ZJzA7vz.png!web

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

缓存预热

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

解决思路

  1. 直接写个缓存刷新页面,上线时手工操作下。

  2. 数据量不大,可以在项目启动的时候自动进行加载。

  3. 定时刷新缓存。

限流

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

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

常见的限流算法

  1. 固定时间窗口算法(计数器)

  2. 滑动时间窗口算法

  3. 令牌桶算法

  4. 漏桶算法

有关限流算法的详细介绍,请 点击查看

总结

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

更多内容,欢迎关注微信公众号:全菜工程师小辉~

FFz2Ef2.png!web

j2uIny6.gif

“阅读原文” 一起来充电吧!

喜欢就点个“在看”呗 ^_^


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK