缓存穿透是指查询一个不存在的数据,由于缓存中没有数据,请求会直接穿透到数据库中,从而引起数据库的压力过大,严重影响系统的性能
缓存击穿是指一个非常热点的数据在缓存中过期之后,正好在这个时间段内有大量的请求访问该数据,这些请求会直接穿透到数据库中,从而引起数据库的压力过大
缓存雪崩是指当缓存中的大量热点数据在同一时间失效,导致大量请求直接访问数据库,从而引起数据库的压力过大,严重影响系统的性能。
①在缓存中存入空对象(假值)
public Double queryLoanInfoHistoryRateAvg() {
//通过工具类常量对应的值,查询缓存获得展示值
Double loanInfoHistoryRateAvg = (Double)redisTemplate.opsForValue().get(Constants.LOAN_INFO_HISTORY_RATE_AVG);
//如果存在直接返回
if(loanInfoHistoryRateAvg!=null){
return loanInfoHistoryRateAvg;
}
if(loanInfoHistoryRateAvg==null) {
//如果缓存中值不存在,访问数据库得到值
loanInfoHistoryRateAvg = loanInfoMapper.selectLoanInfoHistoryRateAvg();
if(loanInfoHistoryRateAvg!=null){
//数据库中值存在,将从数据库中查询出的loanInfoHistoryRateAvg值存入缓存
redisTemplate.opsForValue().set(Constants.LOAN_INFO_HISTORY_RATE_AVG, loanInfoHistoryRateAvg, 20, TimeUnit.SECONDS);
}else{
//数据库中值不存在,存入缓存里一个假的值(或者空对象)
redisTemplate.opsForValue().set(Constants.LOAN_INFO_HISTORY_RATE_AVG, null , 20, TimeUnit.SECONDS);
}
}
return loanInfoHistoryRateAvg;
}
②使用布隆过滤器
可以判断一个元素是否存在于一个集合中,同时也可以减轻数据库的压力。在使用布隆过滤器的时候,首先将所有的数据hash到一个位图中,如果查询的数据在位图中不存在,那么直接返回不存在,从而避免了对数据库的查询操作。
在SpringBoot中,我们可以使用Guava提供的布隆过滤器实现缓存穿透的解决方案。
@Bean
public BloomFilter bloomFilter() {
return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.001);
}
@Override
public User getUserById(String id) {
// 先从布隆过滤器中查询是否存在
if (!bloomFilter.mightContain(id)) {
return null;
}
// 如果存在,则查询Redis中的缓存数据
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则查询数据库
user = userDao.getUserById(id);
if (user != null) {
// 将数据缓存到Redis中
redisTemplate.opsForValue().set(id, user);
} else {
// 如果数据库中也不存在,则将该id加入到布隆过滤器中
bloomFilter.put(id);
}
}
return user;
}
两种解决方式①设置热点数据永不过期
缺点:热点数据可能会被修改,如果不及时更新缓存,可能会导致缓存中的数据与实际数据不一致
@Override
public User getHotUserById(String id) {
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则查询数据库
user = userDao.getHotUserById(id);
if (user != null) {
// 将数据缓存到Redis中,设置过期时间为1小时
redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);
}
}
return user;
}
②双写策略
有大量请求在缓存中查询数据时,1、先在缓存中写入一个空对象,2、然后让一个请求去查询数据库,然后写入缓存,其余请求都去查询缓存。
在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现延迟缓存双写策略的解决方案。例如:
@Override
public User getHotUserById(String id) {
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则写入一个空对象
redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);
// 去数据库中查询数据并更新缓存
user = userDao.getHotUserById(id);
if (user != null) {
redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);
}
}
return user;
}
三种解决方式①缓存数据的随机过期时间
缓存数据设置随机过期时间从而避免大量数据在同一时间失效的情况。在SpringBoot中,我们可以通过设置Redis缓存的过期时间和一个随机值来实现这个解决方案。例如:
@Override
public List<User> getUserList() {
List<User> userList = redisTemplate.opsForValue().get("userList");
if (userList == null) {
// 如果Redis中不存在,则查询数据库
userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
// 将数据缓存到Redis中,并增加随机的过期时间
int random = new Random().nextInt(600) + 600;
redisTemplate.opsForValue().set("userList", userList, random, TimeUnit.SECONDS);
}
}
return userList;
}
②预热缓存
将系统中的热点数据提前加载到缓存中,从而避免了大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过编写一个启动时执行的方法,来实现预热缓存的解决方案。例如:
@Component
public class CacheInit implements CommandLineRunner {
@Autowired
private UserDao userDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) throws Exception {
List<User> userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
// 将数据缓存到Redis中,并设置过期时间为1小时
for (User user : userList) {
redisTemplate.opsForValue().set(user.getId(), user, 1, TimeUnit.HOURS);
}
}
}
}
③分布式锁
使用分布式锁,从而避免大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过Redisson来实现分布式锁的解决方案。例如:
@Override
public List<User> getUserList() {
List<User> userList = redisTemplate.opsForValue().get("userList");
if (userList == null) {
// 如果Redis中不存在,则尝试获取分布式锁
RLock lock = redissonClient.getLock("userListLock");
try {
// 尝试加锁,并设置锁的过期时间为5秒
boolean success = lock.tryLock(5, TimeUnit.SECONDS);
if (success) {
// 如果获取到了锁,则查询数据库并将数据缓存到Redis中
userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
redisTemplate.opsForValue().set("userList", userList, 1, TimeUnit.HOURS);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock
}
} return userList;
}
转载自:SpirngBoot整合Redis解决缓存穿透、缓存击穿、缓存雪崩问题