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);
}
}
整个流程:
如果在 //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);
}
}