MySQL中的单表查询

LeetCode 77、组合

  返回  

缓存击穿!代码实现

2021/7/21 0:07:05 浏览:

之前我们说过了缓存击穿,缓存穿透及缓存雪崩的区别 见
redis缓存雪崩,缓存穿透,缓存击穿场景及解决方案.
今天来谈下具体缓存击穿的解决方案

常规解决 方案1

/**
* @author hm
* @date 2021/7/20
*/
@Service

public class RedisSnowSlideServiceImpl implements RedisSnowSlideService {

   private static final Logger logger = LoggerFactory.getLogger(RedisSnowSlideServiceImpl.class);


   @Autowired
   private UserMapper userMapper;

   @Autowired
   private RedisTemplate<Integer, String> redisTemplate;


   @Override
   public UserInfo getUser(Integer id) {

       //1 先从redis里面查数据
       String userInfoStr = redisTemplate.opsForValue().get(id);
       logger.info("1---【开始】查询数据库--------------");
       //2 如果redis中获取数据为空
       if (isEmpty(userInfoStr)) {
           //从数据库里面查数据
           UserInfo userInfo = userMapper.findById(id);
           //如果数据库为空直接返回
           if (Objects.isNull(userInfo)) {
               return null;
           }
           //不为空就将数据放redis
           userInfoStr = JSON.toJSONString(userInfo);
           logger.info("2---【结束】查询数据库--------------");
           redisTemplate.opsForValue().set(id, userInfoStr);
       }
       return JSON.parseObject(userInfoStr, UserInfo.class);
   }


   private boolean isEmpty(String userInfoStr) {
       return !StringUtils.hasText(userInfoStr);
   }
}

如果,在//1到//2之间耗时1.5秒,那就代表着在这1.5秒时间内所有的查询都会走查询数据库。这也就是我们所说的缓存中的“缓存击穿”。
其实,你们项目如果并发量不是很高,也不用怕,一般这样写也没啥问题,但是一旦在多线程高并发量的情况下,这种写法就会有问题了。那么如何来解决这个问题呢?其实可以通过加锁来解决
下面提供方案二

加锁 方案2

  @Override
    public UserInfo getUser(Integer id) {

        //1 先从redis里面查数据
        String userInfoStr = redisTemplate.opsForValue().get(id);
        logger.info("1---【开始】查询数据库--------------");
        //2 如果redis中获取数据为空
        if (isEmpty(userInfoStr)) {

            synchronized (RedisSnowSlideServiceImpl.class){
                //从数据库里面查数据
                UserInfo userInfo = userMapper.findById(id);
                //如果数据库为空直接返回
                if (Objects.isNull(userInfo)) {
                    return null;
                }
                //不为空就将数据放redis
                userInfoStr = JSON.toJSONString(userInfo);
                logger.info("2---【结束】查询数据库--------------");
                redisTemplate.opsForValue().set(id, userInfoStr);
            }

        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

仔细看这种方案确实是对线程争抢资源做了加锁操作,但是其实并没有解决缓存击穿问题,因为多个线程还是会排队获取到锁,然后执行同步代码块操作
所以需要在synchronized代码块中再次查询下redis,所以引入双重检查锁机制

方案3 双重检查锁

@Override
    public UserInfo getUser(Integer id) {
        //1 先从redis里面查数据
        String userInfoStr = redisTemplate.opsForValue().get(id);
        logger.info("1---【开始】查询数据库--------------");
        //2 如果redis中获取数据为空
        if (isEmpty(userInfoStr)) {

            synchronized (RedisSnowSlideServiceImpl.class){
                userInfoStr = redisTemplate.opsForValue().get(id);

                if (isEmpty(userInfoStr)){
                    //从数据库里面查数据
                    UserInfo userInfo = userMapper.findById(id);
                    //如果数据库为空直接返回
                    if (Objects.isNull(userInfo)) {
                        return null;
                    }
                    //不为空就将数据放redis
                    userInfoStr = JSON.toJSONString(userInfo);
                    logger.info("2---【结束】查询数据库--------------");
                    redisTemplate.opsForValue().set(id, userInfoStr);
                }
            }
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

这种方案其实在正常情况下就没有问题了,但是如果有人恶心攻击,不断的用一个数据库中不存在的数据,比如id为负数或者9999999999这种值,那么这样一来查缓存没有,查数据库也没有,直接返回,再次查缓存,再次查数据库…这样不断循环如果请求量很高也会对数据库造成压力。如何来解决呢?

方案4 设置空对象 (可用)

	 @Override
   public UserInfo getUser(Integer id) {
       //1 先从redis里面查数据
       String userInfoStr = redisTemplate.opsForValue().get(id);
       logger.info("1---【开始】查询数据库--------------");

       if (isEmpty(userInfoStr)) {
           synchronized (RedisSnowSlideServiceImpl.class){
               
               //查询下缓存
               userInfoStr = redisTemplate.opsForValue().get(id);
               
               if (isEmpty(userInfoStr)){
                   //2 查数据库
                   UserInfo userInfo = userMapper.findById(id);
                  
                   if (Objects.isNull(userInfo)) {
                       //设置空对象
                       userInfo = new UserInfo();
                   }
                   userInfoStr = JSON.toJSONString(userInfo);
                   logger.info("2---【结束】查询数据库--------------");
                   redisTemplate.opsForValue().set(id, userInfoStr);
               }

           }

       }
       return JSON.parseObject(userInfoStr, UserInfo.class);
   }

除了上述的设置空对象以为,其实还有种方案,就是通过布隆过滤器。

方案5 布隆过滤器(可用)

布隆过滤器(Bloom Filter):是一种空间效率极高的概率型算法和数据结构用于判断一个元素是否在集合中(类似Hashset)它的核心一个很长的二进制向量和一系列hash函数,数组长度以及hash函数的个数都是动态确定的。其内部维护一个全为0的bit数组
布隆过滤器三个使用场景:
网页爬虫对URL的去重,避免爬取相同的URL地址
反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(垃圾短信)
缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

/**
 * @author hm
 * @date 2021/7/20
 */
@Service
public class RedisSnowSlideServiceImpl implements RedisSnowSlideService {

    private static final Logger logger = LoggerFactory.getLogger(RedisSnowSlideServiceImpl.class);


    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<Integer, String> redisTemplate;
    private static Integer size = 1000000000;

    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);

    /**
     * 布隆过滤器
     * @param id
     * @return
     */
    @Override
    public UserInfo getUser(Integer id) {
        //1 先从redis里面查数据
        String userInfoStr = redisTemplate.opsForValue().get(id);
        logger.info("1---【开始】查询数据库--------------");
        if (isEmpty(userInfoStr)) {
            //校验是否在布隆过滤器中
            if (bloomFilter.mightContain(id)){
                return null;
            }
            
            synchronized (RedisSnowSlideServiceImpl.class){

                //查询下缓存
                userInfoStr = redisTemplate.opsForValue().get(id);

                if (isEmpty(userInfoStr)){
                    if (bloomFilter.mightContain(id)){
                        return null;
                    }
                    
                    //2 查数据库
                    UserInfo userInfo = userMapper.findById(id);

                    if (Objects.isNull(userInfo)) {
                        //将id对应的空值放入布隆过滤器
                        bloomFilter.put(id);
                    }
                    userInfoStr = JSON.toJSONString(userInfo);
                    logger.info("2---【结束】查询数据库--------------");
                    redisTemplate.opsForValue().set(id, userInfoStr);
                }
            }
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

方案6 分布式锁

    /**
     * 分布式锁
     * @param id
     * @return
     */
    @Override
    public UserInfo getUser(Integer id) {
        //1 先从redis里面查数据
        String userInfoStr = redisTemplate.opsForValue().get(id);
        logger.info("1---【开始】查询数据库--------------");
        if (isEmpty(userInfoStr)) {

            //缓存未命中 查询数据库
            String lockKey = "lock" + id;
            RLock lock = redisson.getLock(lockKey);
            lock.lock();
            if (lock.isLocked()) {
                //2 查数据库
                try {
                    UserInfo userInfo = userMapper.findById(id);
                    if (Objects.isNull(userInfo)){
                        return null;
                    }
                    userInfoStr = JSON.toJSONString(userInfo);
                    redisTemplate.opsForValue().set(id, userInfoStr);
                } finally {
                    lock.unlock();
                }

            }else{
                //查询下缓存
                userInfoStr = redisTemplate.opsForValue().get(id);
                if (isEmpty(userInfoStr)){
                    return null;
                }

            }

        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号