缓存穿透
介绍
缓存穿透是指客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,失去了缓存保护数据库的意义.
常见的解决方案有两种:
- 缓存空对象:当我们客户端访问不存在的数据时,先请求redis,但此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中,设置一个较短的过期时间.这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了.但是如果这条记录后来在数据库中存在对应的记录,就会造成数据短暂的不一致
- 布隆过滤器:布隆过滤器详解博客
代码实现
/**
* @author fanqiechaodan
* @Classname UserServiceImpl
* @Description
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 储存用户信息的key前缀
*/
private static final String REDIS_USER_PREFIX = "user_";
@Override
public User getUser(String id) {
String resStr = stringRedisTemplate.opsForValue().get(REDIS_USER_PREFIX + id);
if (StringUtils.isNotBlank(resStr)) {
log.info("从redis获取返回...");
return JSON.parseObject(resStr, User.class);
}
if (StringUtils.EMPTY.equals(resStr)) {
log.info("缓存虽然命中,但是为StringUtils.EMPTY,直接返回null");
return null;
}
log.info("从mysql获取...");
User res = userMapper.getById(id);
if (Objects.isNull(res)) {
log.info("从mysql获取到的仍然为null,缓存空对象,设置较短过期时间");
stringRedisTemplate.opsForValue().set(REDIS_USER_PREFIX + id, StringUtils.EMPTY, 60, TimeUnit.SECONDS);
return res;
}
log.info("从mysql获取到的不为null设置较长的过期时间");
stringRedisTemplate.opsForValue().set(REDIS_USER_PREFIX + id, JSON.toJSONString(res), 5, TimeUnit.MINUTES);
return res;
}
}
缓存雪崩
介绍
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力.
常用的解决方案:
- 给不同key的TTL添加随机值
- 利用redis集群提高服务的可用性
代码实现
/**
* @author fanqiechaodan
* @Classname UserServiceImpl
* @Description
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 储存用户信息的key前缀
*/
private static final String REDIS_USER_PREFIX = "user_";
@Override
public User getUser(String id) {
String resStr = stringRedisTemplate.opsForValue().get(REDIS_USER_PREFIX + id);
if (StringUtils.isNotBlank(resStr)) {
log.info("从redis获取返回...");
return JSON.parseObject(resStr, User.class);
}
log.info("从mysql获取...");
User res = userMapper.getById(id);
if (Objects.nonNull(res)) {
log.info("从mysql获取到的不为null设置过期时间(5-10之间的随机数)");
long timeout = (int) (Math.random() * 5 + 5);
stringRedisTemplate.opsForValue().set(REDIS_USER_PREFIX + id, JSON.toJSONString(res), timeout, TimeUnit.MINUTES);
}
return res;
}
}
缓存击穿
介绍
缓存击穿就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击.
常见的解决方案就是使用互斥锁来解决.因为锁能实现互斥性,假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去查询数据库构建缓存,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行休眠,然后重试,直到线程1重新构建完缓存把锁释放返回后,线程2再来执行,此时就能从缓存中拿到数据了.
代码实现
/**
* @author fanqiechaodan
* @Classname UserServiceImpl
* @Description
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 储存用户信息的key前缀
*/
private static final String REDIS_USER_PREFIX = "user_";
/**
* redis加锁前缀
*/
private static final String REDIS_LOCK_PREFIX = "lock_";
@Override
public User getUser(String id) {
String resStr = stringRedisTemplate.opsForValue().get(REDIS_USER_PREFIX + id);
if (StringUtils.isNotBlank(resStr)) {
log.info("从redis获取返回...");
return JSON.parseObject(resStr, User.class);
}
// 使用redis进行加锁
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_PREFIX + id, "1", 10, TimeUnit.SECONDS);
User res = null;
try {
if (lockFlag) {
log.info("加锁成功,重新构建缓存...");
res = userMapper.getById(id);
stringRedisTemplate.opsForValue().set(REDIS_USER_PREFIX + id, JSON.toJSONString(res), 5, TimeUnit.MINUTES);
} else {
log.info("加锁失败,休眠进行重试...");
Thread.sleep(50);
getUser(id);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (lockFlag) {
stringRedisTemplate.delete(REDIS_LOCK_PREFIX + id);
}
}
return res;
}
}