目录
学习目标
学习并解决redis缓存三类问题
学习内容
在我们在查询数据时,在没有使用类似redis缓存的情况下,一般都是直接去数据库中查询,但是直接查询数据库的效率比使用redis缓存来查询数据的效率要低,所以我们需要增加缓存,但是使用redis缓存就存在一些问题,这里是对使用redis缓存的三类问题的总结。
缓存穿透
问题描述
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决方案
- 缓存空对象
当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库。为防止这个问题的发生,对于访问的这个数据在数据库中即使不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据。
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
public <R,ID> R queryWithPassThrough(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){
//1.从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//2.判断是否存在
if(StrUtil.isNotBlank(json)){
//3.存在,直接返回
return JSONUtil.toBean(json, type);
}
//判断命中的是否为空值
if(json!=null){
//这个是用来反馈信息的
return null;
}
//4.不存在,根据id查询数据库
//交给调用者
R r = dbFallback.apply(id);
//5.不存在返回错误
if(r==null){
//空值写入redis
stringRedisTemplate.opsForValue().set(keyPrefix+id,"",time,unit);
return null;
}
//6.存在写入redis
this.set(keyPrefix+id,r,time,unit);
//7.返回
return r;
}
- 布隆过滤
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲
这个暂时没有实现代码。。。。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
-
给不同的Key的TTL添加随机值
-
给缓存业务添加降级限流策略
目前暂时只知道第一种方案就挺好的了,就是在导入缓存key的时候,给key设置不同的过期时间就行了。
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
-
互斥锁
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行。对于锁的实现可以通过redis中的setnx来实现。
当第一个人访问时,缓存并未命中,第一个人就需要拿到锁并从数据库中取出数据在redis中重新缓存数据,在第一个没有释放锁时,其他人的访问请求只能等待,直到第一个人释放锁后,在redis中可以查询到数据。
//redis加锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);//直接返回会发生拆箱,有可能会返回null
}
//释放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
/**
* 缓存击穿,互斥锁处理
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
//1.从redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否为空值
if(shopJson!=null){
//这个是用来反馈信息的
return null;
}
//4.不存在,根据id查询数据库
//实现缓存重建
//获取锁
Shop shop= null;
String lockKey="lock:shop:0"+id;
try {
boolean isLock=tryLock(lockKey);
//判断是否获取成功
if(!isLock){
// 失败,休眠并重试
Thread.sleep(50);
queryWithMutex(id);
}
//获取锁成功之后应对redis缓存在做一次判断,防止其余等待的线程重复查询数据库
//1.从redis查询缓存
shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,直接返回
shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
shop = getById(id);
//5.不存在返回错误
if(shop==null){
//空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//6.存在写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放互斥锁
unLock(lockKey);
}
//7.返回
return shop;
}
-
逻辑过期
我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据就可以一直占用我们内存了。我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。
假设第一个人去查询缓存,然后从value中判断出来当前的数据已经过期了,第一个人获取互斥锁,获得了锁的线程他会开启一个 线程去进行 更新缓存的数据,直到新开的线程完成这个逻辑后,才释放锁。在更新数据的期间,其他人来访问数据时,直接返回redis中未更新的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
//设置逻辑过期时间
public void setWithExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData redisData=new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入reids
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
/**
* 缓存击穿逻辑过期处理
* @param id
* @return
*/
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){
//1.从redis查询缓存
String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//2.判断是否存在
if(StrUtil.isBlank(Json)){
//3.存在,直接返回
return null;
}
//命中,需要把json反序列化为对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r=JSONUtil.toBean(data,type);
LocalDateTime expireTime=redisData.getExpireTime();
//判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期,直接返回店铺对象
return r;
}
//已过期,开始缓存重建
//获取锁
String lockKey=keyPrefix+id;
boolean flag = tryLock(lockKey);
//判断获取是否成功
if(flag){
//此处应对redis缓存再做一次判断
//TODO 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
//查询数据
R r1= dbFallback.apply(id);
//写入redis
this.setWithExpire(keyPrefix+id,r1,time,unit);
//释放锁
unLock(lockKey);
});
}
//失败返回商铺信息
return r;
}
到此为止,上述内容就是这段时间我所学习的reids缓存相关的知识。
此文章仅供本人学习使用,如有问题,欢迎各位大佬指正与交流。