第九节 缓存穿透

一、缓存穿透概念

        缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。

        因此,如果如果查询一个不存在的key,此流程会查询一次Redis缓存,再查询一次DB。如果短时间内出现大量请求,那么会对我们的Redis缓存系统以及数据库造成巨大的压力,甚至宕机。

二、实例代码

         如下代码,首先通过用户的ID计算出KEY,然后通过KEY判断是否在Redis中存在。如果不存在,然后会查询一次数据库。如果极短时间内发送了1000次查询ID=1的用户,且ID=1的用户数据库中也不存在,那么就是就走1000次缓存,1000次数据库。这样的话,很容易把数据库搞崩溃。

        String key = param.getId().toString();
        //查询的时候,先查询Redis缓存,客户端肯定会传一个ID过来
        if (stringRedisService.exists(key)) {
            LOGGER.info("从Redis缓存中查询User");
            String result = stringRedisService.get(key);
            return objectMapper.readValue(result, User.class);

        } else {
            LOGGER.info("从数据库中查询User");
            User user =  userMapper.selectUserById(param.getId());
            if(user!=null){
               redisService.set(key,user);  
            }
            return user;
        }

        解决办法一:把不存在的记录的缓存,作为空字符串存放到Redis中,下次通过同一个KEY查询Redis会得到一个空字符串。如果是空字符的话,就说明此记录不存在于数据库中,直接返回null给前端。

        String key = param.getId().toString();
        //查询的时候,先查询Redis缓存,客户端肯定会传一个ID过来
        if (stringRedisService.exists(key)) {
            LOGGER.info("从Redis缓存中查询User");
            String result = stringRedisService.get(key);
            //如果不是空字符串
            if (StringUtils.isNotBlank(result)) {
                return objectMapper.readValue(result, User.class);
            }
            //是空字符串,说明缓存中存放了空字符串
            // 因此返回null,防止缓存穿透
            return null;
            
        } else {
            LOGGER.info("从数据库中查询User");
            User result = userMapper.selectUserById(param.getId());
            if (result != null) {
                  stringRedisService.set(key, result);
            }else{
                  stringRedisService.set(key, "");
                  //TODO:这里还可以设置过期时间
            }
            return result;
        }

        解决办法二:限流。如果是单节点部署的的情况下,可以使用谷歌提供的限流工具。如下代码,RateLimiter对象每秒产生2个令牌到令牌池里,也就意味着同一秒内,最多有两个请求能够进入Service层。这样就能限制同一时刻巨量请求倾泻到我们的缓存系统或者数据库中。

import com.google.common.util.concurrent.RateLimiter;

 /**
     * 每秒创建两个令牌
     */
    private final RateLimiter RATE_LIMITER = RateLimiter.create(2);
    
    @GetMapping(value = "/getUser")
    public User getUser(String key) {
        try {
            //此请求需要1个令牌,否则返回false
            if (RATE_LIMITER.tryAcquire(1)) {
                return userItemService.getUser();
            }else{
                return null;
            }
        } catch (Exception e) {
            throw new InvalidArgumentException(500, "获取失败");
        }
    }
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0.1-jre</version>
        </dependency>
发布了317 篇原创文章 · 获赞 250 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/yanluandai1985/article/details/104713904