所谓的缓存穿透,是指要查询的数据既不在缓存中,也不再数据库中,此时就会导致缓存永远失效,并且请求都会达到数据库中,从而增加了数据库的查询次数。
这时候我们主要有以下几种方式来解决:
① 返回一个空对象: 当发现数据库中找不到这个数据的时候,那么为了避免下一次再次请求这个数据的时候,还需要访问数据库,这时候我们将一个空串保存到redis中,这样就可以保证了下一次可以从缓存中取出并判断出这个数据并不存在数据库中。
②布隆过滤:也就是在从缓存中查询数据之前,会先通过布隆过滤器来进行查询,判断这个数据是否存在,如果不存在,那么就直接告诉前端,提示数据不存在,否则就查询缓存,如果缓存中也不存在,那么就需要取查询数据库了。
所以这里我们将采用返回一个空对象的方式,来解决该项目中的缓存穿透问题, 对应的步骤为:
所以对应的代码为:
@Override
public Result queryShopById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
//1、查询redis中的数据
String jsonString = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isNotBlank(jsonString)){
//isNotBlank :表示只有在字符串不为null,并且长度不为0,并且不含有空白字符的时候,才返回true,否则返回false
//2、redis中店铺存在,直接返回
return Result.ok(JSONUtil.toBean(jsonString, Shop.class));
}
if(jsonString != null && jsonString.length() <= 0){
//如果jsonString不等于null,并且长度为0,说明数据库中不存在这个数据
//但是在缓存中保存这个空串,从而防止缓存穿透
return Result.fail("店铺不存在");
}
//3、店铺不存在,查询数据库
Shop shop = getById(id);
if(shop == null){
//3.1查询数据为空,将空数据保存到缓存中
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
//3.2 返回错误信息
return Result.fail("店铺不存在");
}
//3.3 将数据保存到redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//4、将数据返回
return Result.ok(shop);
}
所谓的缓存雪崩,就是在同一个时间内,大量的缓存key同时失效,或者服务器发生了宕机(此时所有的缓存key都已经失效了),此时所有的请求都会到达数据库,从而增加了数据库的查询次数。
解决方案主要有以下几种:
① 设置缓存key的TTL为一个随机数,从而避免大量的key在同一个时间失效。例如在批量设置key的TTL的时候,我们在设置的TTL的基础上,加一个随机数,从而避免了各个key的TTL相同的情况。也即最终key的TTL等于 原来的TTL + 随机数.
② 要解决服务器发生宕机这种情况,则需要利用集群来解决
③ 给缓存服务添加多级缓存
④给缓存服务添加降级限流策略
所谓的缓存击穿,也叫做热点问题,指的是一个被高并发访问,并且缓存重建业务比较复杂的key突然失效,无数的请求都将到达数据库,此时就会导致数据库的访问次数增多, 如下所示:
所以对应的解决方法主要有以下2中方式:
① 互斥锁: 如上面所示,当线程1在得知缓存没有命中的时候,线程1就去查询数据库,然后重建缓存数据,之后再写入缓存,由于重建缓存业务复杂,所以花费的时间相对较长,这时候如果有线程2,3,4也要查询这个数据,那么他们也需要取查询数据库了。所以我们只需要在线程1执行查询数据库操作之前,设置一个互斥锁,如果线程1能够获得这个互斥锁,那么就可以取执行查询数据库,并且重建缓存业务操作,否则,如果线程2,3,4没有获得互斥锁,则需要睡眠一段时间之后,再次查询缓存是否命中,当线程1执行了操作之后,就需要将互斥锁释放,而这时候的线程2,3,4都可以查询到缓存了,从而解决了缓存击穿的问题, 对应的步骤如下所示:
对应的代码为:
/**
* 通过互斥锁的方式来解决缓存击穿问题,对应的步骤为:
* 1、查询缓存,判断是否可以命中,如果可以命中,那么将数据返回
* 2、不能命中,那么调用方法tryLock,从而判断是否可以获得互斥锁
* 3、如果能够获得互斥锁,那么这个线程就去查询数据库,并且将重建缓存业务操作
* 4、否则,如果不能获得互斥锁,那么说明已经有其他线程去查询数据库,执行重建
* 缓存业务操作了,此时这个线程只需要睡眠一段时间,然后再次查询缓存即可
* 5、当重建缓存业务操作完成之后,就需要释放互斥锁
* 6、返回数据
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
String key = RedisConstants.CACHE_SHOP_KEY + id;
//1、查询redis中的数据
String jsonString = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isNotBlank(jsonString)){
//isNotBlank :表示只有在字符串不为null,并且长度不为0,并且不含有空白字符的时候,才返回true,否则返回false
//2、redis中店铺存在,直接返回
return JSONUtil.toBean(jsonString, Shop.class);
}
if(jsonString != null && jsonString.length() <= 0){
//如果jsonString不等于null,并且长度为0,说明数据库中不存在这个数据
//但是在缓存中保存这个空串,从而防止缓存穿透
return null;
}
//3、店铺不存在,判断是否可以获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;//每个商铺都有一个互斥锁
Boolean isLock = tryLock(lockKey);
try{
if(!isLock){
//不能获得互斥锁,将这个线程睡眠一段时间,然后重新查询缓存
Thread.sleep(50);
return queryWithMutex(id);
}
//为了保证重建业务相对较长,所以将这个线程睡眠200ms
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4、能够获得互斥锁,查询数据库
Shop shop = getById(id);
if(shop == null){
//4.1查询数据为空,将空数据保存到缓存中
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
//4.2 返回null
return null;
}
//4.3 将数据保存到redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//5、释放互斥锁
releaseLock(lockKey);
//6、将数据返回
return shop;
}
/**
* 判断是否可以获取互斥锁,此时只需要通过setnx这个方法来实现即可
* 如果这个方法能够成功执行,说明可以获取互斥锁,否则不可以。
* 因为setnx表示的是:只有这个key不存在,才可以添加这个key,否则不可以
* 返回true,表示可以执行这个方法,表示可以获得互斥锁, 否则不可以获得互斥锁
* @param lockKey
* @return
*/
public Boolean tryLock(String lockKey){
//这时候的setIfAbsent可能返回的是null,所以需要通过BooleanUtils来解决
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.MINUTES);
//判断redis是否已经存入了这个key,虽然可以打印出来,但是在客户端中却看不到这个key
//System.out.println("redis 中lockKey得知为: " + stringRedisTemplate.opsForValue().get(lockKey));
return BooleanUtil.isTrue(flag);
}
/**
* 释放互斥锁
* @param lockKey
*/
public void releaseLock(String lockKey){
stringRedisTemplate.delete(lockKey);
}
② 逻辑过期: 将数据添加到缓存中的时候,是永不过期的,只是这个key里面的数据多出了一项,来记录它的过期时间。当有线程从缓存中取出这个数据的时候,那么就根据这个key中的这个过期时间,判断是否过期,如果已经过期,那么就另外开一个线程,来进行重建缓存业务操作,而当前这个线程则将没有更新的数据返回给前端,对应的步骤如下所示:
对应的代码为:
private final ExecutorService service = Executors.newFixedThreadPool(10);
/**
* 通过逻辑过期来解决缓存击穿问题,对应的步骤为:
* 1、查询缓存,是否命中
* 2、如果不能命中,说明数据库中并不存在这个数据,直接返回null
* 3、如果命中,这时候需要判断逻辑过期时间是否已经过期了
* 4、逻辑过期时间还没有到,那么直接将数据返回
* 5、逻辑过期时间已经到了,那么判断是否可以获取互斥锁,
* 6、可以获取互斥锁,那么就另外开一条线程,用来查询数据库,以及重建缓存
* 业务操作
* 7、本线程直接将数据返回
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id){
//1、从缓存中取出数据
String key = RedisConstants.CACHE_SHOP_KEY + id;
String jsonStr = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isBlank(jsonStr)){
//数据为空,说明不能命中,说明数据库中并没有这个数据,直接返回null
return null;
}
//2.1 命中,那么获取过期时间,首先将jsonStr转成RedisData类型
RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
System.out.println("redisData = " + redisData);
//2.2 获取逻辑过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//因为RedisData中的data是一个Object类型,所以当反序列化,也即toBean方法调用之后,
//就会将这一串数据的jsonStr变成JSONObject返回,此时就不可以强转成为Shop
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//2.3 判断是否已经过了逻辑时间
if(expireTime.isAfter(LocalDateTime.now())){
//2.4 没有过期,直接将数据返回
return shop;
}
//3 已经过期,那么判断是否可以获得互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
Boolean isLock = tryLock(lockKey);
if(isLock){
//3.1 可以得到互斥锁,那么要另外开启一条线程,进行重建缓存任务,
//这里通过线程池来完成
service.execute(() -> {
try{
//3.2 重建缓存
save2Shop(id, 20L);
}catch(RuntimeException e){
throw new RuntimeException(e);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//3.3 释放互斥锁
releaseLock(lockKey);
}
}
);
}
//4 将数据返回即可
return shop;
}
public void save2Shop(Long id, Long expiredTime) throws InterruptedException {
//1、查询数据库
Shop shop = getById(id);
Thread.sleep(200);
//2、添加到缓存中,并且为了解决缓存击穿的问题,需要添加一个逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expiredTime));
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}