Redis代码系列:出现缓存击穿! 代码该如何写?

Redis存在三大问题:缓存雪崩、缓存击穿、缓存穿透。 今天我们将讨论缓存击穿时,代码该如何写?

场景:

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public UserInfo findById(Long id) {
        //查询缓存
        String userInfoStr = redisTemplate.opsForValue().get(id);
        //如果缓存中不存在,查询数据库
        // A
        if (isEmpty(userInfoStr)) {
            UserInfo userInfo = userMapper.findById(id);
            //数据库中不存在
            if(userInfo == null){
                  return null;
            }
            userInfoStr = JSON.toJSONString(userInfo);
            //放入缓存
            // B
            redisTemplate.opsForValue().set(id, userInfoStr);
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

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

整个流程:

Redis代码系列:出现缓存击穿! 代码该如何写?_第1张图片

如果在 //A 和 ///B之间需要 1.5 秒,则表示这 1.5 秒内的所有查询都会去查询数据库。 这就是我们所说的缓存中的“缓存击穿”。 其实如果你的项目并发度不是很高,不要怕,我见过很多项目都是这样写的,没有那么多。 毕竟,这可能是第一次发生。 发生缓存击穿。  但是,让我们不要以侥幸的心态编写代码。 既然是多线程引起的,估计很多人都会想到锁。 让我们用锁来解决这个问题。

改进版:

既然使用了锁,那么我们首先要关心的是锁的粒度。  如果我们把它放在方法findById上,就意味着所有的查询都会有锁竞争。 到这里相信大家都知道为什么我们不把它放在方法上。

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;

    @Autowired
    public UserInfo findById(Long id) {
        //查询缓存
        String userInfoStr = redisTemplate.opsForValue().get(id);
        if (isEmpty(userInfoStr)) {
            //只有不存的情况存在锁
            synchronized (UserInfoServiceImpl.class){
                UserInfo userInfo = userMapper.findById(id);
                //数据库中不存在
                if(userInfo == null){
                     return null;
                }
                userInfoStr = JSON.toJSONString(userInfo);
                //放入缓存
                redisTemplate.opsForValue().set(id, userInfoStr);
            }
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

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

看来问题已经解决了。 其实问题还是没有解决,会出现缓存崩溃,因为排队获取锁后,还是会执行同步块代码,也就是查询数据库,缓存击穿没有解决根本。  

双校验锁
因此我们引入了双校验锁,在之前的版本中稍微做了改动,在同步模块中再次检查缓存中是否存在。

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public UserInfo findById(Long id) {
        //查缓存
        String userInfoStr = redisTemplate.opsForValue().get(id);
        //第一次校验缓存是否存在
        if (isEmpty(userInfoStr)) {
            //上锁
            synchronized (UserInfoServiceImpl.class){ 
                //再次查询缓存,目的是判断是否前面的线程已经set过了
                userInfoStr = redisTemplate.opsForValue().get(id);
                //第二次校验缓存是否存在
                if (isEmpty(userInfoStr)) {
                    UserInfo userInfo = userMapper.findById(id);
                    //数据库中不存在
                    if(userInfo == null){
                        return null;
                    }
                    userInfoStr = JSON.toJSONString(userInfo);
                    //放入缓存
                    redisTemplate.opsForValue().set(id, userInfoStr);
                }
            }
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

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

这样看来,我们解决了缓存崩溃的问题,你觉得解决了吗?  

恶意攻击
回想以上案例,一般情况下是可以的,但是一旦有人恶意攻击呢?  

例如:输入参数id=10000000,数据库中没有这个id,怎么办?  
第一步,缓存中不存在 
第二步,查询数据库 
第三步,由于数据库不存在,直接返回 操作缓存
第四步,再次执行第一步.....有一个无限循环

方案一:设置一个空对象
是当缓存和 数据库不存在 在这种情况下,id 是键,空对象是值。  
set(id, 空对象);  
回到上面四步就变成了。  
例如:输入参数id=10000000,数据库中没有这个id,怎么办?  
第一步,缓存中不存在 
第二步,查询数据库 
第三步,因为数据库不存在,使用id作为 key 和空对象 as 把值放入缓存 
第四步,执行第一步。 此时缓存是存在的,但此时只是一个空对象。

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate; 

    @Override
    public UserInfo findById(Long id) {
        String userInfoStr = redisTemplate.opsForValue().get(id);
        //判断缓存是否存在,是否为空对象
        if (isEmpty(userInfoStr)) {
            synchronized (UserInfoServiceImpl.class){
                userInfoStr = redisTemplate.opsForValue().get(id);
                if (isEmpty(userInfoStr)) {
                    UserInfo userInfo = userMapper.findById(id);
                    if(userInfo == null){
                        //构建一个空对象
                        userInfo= new UserInfo();
                    }
                    userInfoStr = JSON.toJSONString(userInfo);
                    redisTemplate.opsForValue().set(id, userInfoStr);
                }
            }
        }
        UserInfo userInfo = JSON.parseObject(userInfoStr, UserInfo.class);
        //空对象处理
        if(userInfo.getId() == null){
            return null;
        }
        return JSON.parseObject(userInfoStr, UserInfo.class);
    }

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

你可能感兴趣的:(redis系列,java,redis,后端,缓存)