缓存击穿!竟然不知道怎么写代码???

Redis中有三大问题:缓存雪崩缓存击穿缓存穿透,今天我们来聊聊缓存击穿

关于缓存击穿相关理论文章,相信大家已经看过不少,但是具体代码中是怎么实现的,怎么解决的等问题,可能就一脸懵逼了。

今天,老田就带大家来看看,缓存击穿解决和代码实现。

场景

请看下面这段代码:

/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate redisTemplate; @Override public UserInfo findById(Long id) { //查询缓存 String userInfoStr = redisTemplate.opsForValue().get(id); //如果缓存中不存在,查询数据库 //1 if (isEmpty(userInfoStr)) { UserInfo userInfo = userMapper.findById(id); //数据库中不存在 if(userInfo == null){ return null; } userInfoStr = JSON.toJSONString(userInfo); //2 //放入缓存 redisTemplate.opsForValue().set(id, userInfoStr); } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }登录后复制

缓存击穿!竟然不知道怎么写代码???

如果,在//1//2之间耗时1.5秒,那就代表着在这1.5秒时间内所有的查询都会走查询数据库。这也就是我们所说的缓存中的“缓存击穿”。

其实,你们项目如果并发量不是很高,也不用怕,并且我见过很多项目也就差不多是这么写的,也没那么多事,毕竟只是第一次的时候可能会发生缓存击穿。

但,我们也不要抱着一个侥幸的心态去写代码,既然是多线程导致的,估计很多人会想到锁,下面我们使用锁来解决。

改进版

既然使用到锁,那么我们第一时间应该关心的是锁的粒度。

如果我们放在方法findById上,那就是所有查询都会有锁的竞争,这里我相信大家都知道我们为什么不放在方法上。

/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate redisTemplate; @Override 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); } }登录后复制

双重检查锁

由此,我们引入双重检查锁,我们在上的版本中进行稍微改变,在同步模块中再次校验缓存中是否存在。

/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource 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,怎么办呢?

第一步、缓存中不存在

第二步、查询数据库

第三步、由于数据库中不存在,直接返回了,并没有操作缓存

第四步、再次执行第一步.....死循环了吧

方案1:设置空对象

就是当缓存中和数据库中都不存在的情况下,以id为key,空对象为value。

set(id,空对象);

回到上面的四步,就变成了。

比如说:入参id=10000000,在数据库里并没有这个id,怎么办呢?

第一步、缓存中不存在

第二步、查询数据库

第三步、由于数据库中不存在,以id为key,空对象为value放入缓存中

第四步、执行第一步,此时,缓存就存在了,只是这时候只是一个空对象。

代码实现部分:

/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource 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); } }登录后复制