Redis拾遗(八)——缓存穿透简介及解决方案

这篇博客在之前实例博客的基础上,总结一下缓存穿透的问题。这篇博客依旧会在之前构建实例项目的基础上进行

何为缓存穿透?

其实从字面意思也不难理解,缓存穿透其实就是查询的数据压根就不在缓存和数据库中,**但是用户的查询请求依旧会走到数据库上去,导致数据库需要处理大量无意义的查询请求,如果同一时间出现很多这种请求,数据库貌似有点扛不住。**这样造成的后果也是很明显的,数据库的查询压力会很大,严重的会出现宕机。

我们正常的写法如下:

/**
 * 模拟缓存穿透的场景
 * @param id
 * @return
 * @throws Exception
 */
public Item getItem(Integer id) throws Exception {
    Item item = null;
    if(id!=null){
        if(stringRedisService.exist(id.toString())){
            String result = stringRedisService.get(id.toString()).toString();
            if(StrUtil.isNotBlank(result)){
                item = objectMapper.readValue(result,Item.class);
            }
        }else{
            log.info("----缓存穿透,从数据库查询----");
            item = itemMapper.selectByPrimaryKey(id);
            if(item!=null){
               //如果数据库中查询到记录,则更新缓存
               stringRedisService.put(id.toString(),objectMapper.writeValueAsString(item));
            }
        }
    }
    return item;
}

之后我们利用JMeter模拟一下多个线程同时访问这段代码的情况,在之前介绍分布式锁的时候,提过利用JMeter如何模拟多个线程测试的方法,具体如下:JMeter 多线程测试

针对上述代码(controller的代码这里不再贴出),我们也采用同样的方式来进行测试,最后可以看到如下结果

Redis拾遗(八)——缓存穿透简介及解决方案_第1张图片

大量的请求依旧进入到的数据库,且并没有真正查出数据。

解决方案一——存一个空值

通过分析,主要是原因是因为数据库也没有对应的数据,导致查询请求一路穿插到底了。解决方案之一,其实就是将查询不到的空值,也作为一个缓存存入到Redis

/**
 * 模拟缓存穿透的场景 解决方案一
 * @param id
 * @return
 * @throws Exception
 */
public Item getItemSolutionOne(Integer id) throws Exception {
    Item item = null;
    if(id!=null){
        if(stringRedisService.exist(id.toString())){
            String result = stringRedisService.get(id.toString()).toString();
            if(StrUtil.isNotBlank(result)){
                //将查询出来的数据,json序列化之后存储
                item = objectMapper.readValue(result,Item.class);
            }
        }else{
            log.info("----缓存穿透,从数据库查询解决方案一,解决方案一----");
            item = itemMapper.selectByPrimaryKey(id);
            if(item!=null){
                stringRedisService.put(id.toString(),objectMapper.writeValueAsString(item));
            }else{
                //如果数据库查不到,就直接将空字符串存入缓存,这里还不能存入null,因为存入null,序列化后会成为"null"的字符串
               //不能这么写 //stringRedisService.put(id.toString(),objectMapper.writeValueAsString(item));
                //stringRedisService.put(id.toString(),objectMapper.writeValueAsString(""));//空串序列化后会成为""""
                //这里不再序列化
                stringRedisService.put(id.toString(),"");
                stringRedisService.expire(id.toString(),3600L);//同时设置一个过期时间
            }
        }
    }
    return item;
}

值得一提的是,在存入空对象的时候,直接存入"“即可,而不是存入null或者序列化空串,因为这样会导致Json解析出现问题,JSON序列化会将null序列化为"null”。

但是上述解决方案依旧存在问题,如果并发线程数足够大,在刚开始的时候,依旧会有很多线程,直接去查询数据库

Redis拾遗(八)——缓存穿透简介及解决方案_第2张图片

如果请求数量非常大,在空数据还没有写入到缓存的时候,很多线程依旧无法从缓存中读取数据,同一时刻这些请求依旧会直接请求到数据库,因此这个方案似乎还缺点什么。这里就要引入限流了。

解决方案二——引入限流

引入限流(这里只是用guava的限流组件,如果真正想模拟分布式环境下的限流,则需要引入springcloud的hystrix限流组件),在解决方案一中说到,短时间内会有很多请求访问,在那一瞬间数据库的压力依旧不小。为解决次问题,需要引入限流。

这里采用guava的RateLimiter来实现限流策略,详细可以参见该篇博客 guava RateLimiter介绍。

//初始化RateLimiter
@Autowired
private static final RateLimiter rateLimiter = RateLimiter.create(0.05);

/**
 * 缓存穿透第二种解决方案
 * 加入限流
 *
 * @param id
 * @return
 */
public Item getItemSolutioinTwo(Integer id) throws Exception {
    Item item = null;
    if (id != null) {
        if (stringRedisService.exist(id.toString())) {
            log.info("缓存命中,从缓存查询数据结果");
            String result = stringRedisService.get(id.toString()).toString();
            if (StrUtil.isNotBlank(result)) {
                item = objectMapper.readValue(result, Item.class);
            }
        } else {
            //针对查询数据库的方面,加入限流操作
            if (rateLimiter.tryAcquire()) {
                log.info("----缓存穿透次数:{},从数据库查询[解决方案二]----",limitCount++);
                item = itemMapper.selectByPrimaryKey(id);
                if (item != null) {
                    stringRedisService.put(id.toString(), objectMapper.writeValueAsString(item));
                } else {
                    stringRedisService.put(id.toString(), "");
                    stringRedisService.expire(id.toString(), 3600L);//同时设置一个过期时间
                }
            }
        }
    }
    return item;
}

运行之后,结果如下:

Redis拾遗(八)——缓存穿透简介及解决方案_第3张图片

可以看到,只发生了一次缓存穿透,之后均能正常从缓存中获取数据(话说测试这个的时候,电脑都运行的更快了)

其他解决方案

其实还有其他解决方案,比如将数据库中所有的记录存入到Redis的一个set中(这个方法有点恐怖),或者引入布隆过滤器(redission提供了一个集成的)。这里不一一介绍。

缓存击穿与缓存雪崩

其实缓存穿透、缓存击穿、缓存雪崩,实质都是一样的,简单点说都是缓存中查询不到数据,导致数据库压力过大,但是需要考虑的是在某一时刻,如果缓存中大量数据失效,就会有大量请求被送到数据库,这样在那一瞬间数据库的压力是很大的。因此需要有类似限流一样的策略来补充。

总结

本篇博客根据实例简单总结了一下缓存穿透,缓存击穿,缓存雪崩,其实质都是一样。但是需要注意哪一瞬间数据库的压力,该如何解决。

你可能感兴趣的:(分布式,redis)