缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
优点:实现简单,维护方便
缺点: 额外的内存消耗 可能造成短期的不一致(恶意攻击的对象id对应的redis空值缓存失效前成为了新插入的id,造成真实客户只能得到空缓存)
布隆过滤(类似于位示图0、1代表是否存在)
优点:内存占用较少,没有多余key
缺点: 实现复杂 存在误判可能(对于过滤结果:假的一定为假,真的有小概率为假)
缓存穿透的解决方案有哪些?
缓存null值
布隆过滤
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
代码解释,这是一个通用的工具类方法:
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key=keyPrefix+id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json,type);
}
if (json!=null){
return null;
}
R apply = dbFallback.apply(id);
if (apply==null){ //生成空值缓存,避免缓存穿透
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES); //对象转Json
return null;
}
this.set(key,JSONUtil.toJsonStr(apply),time, unit);
return apply;
}
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
互斥锁、逻辑过期
基于代码实现互斥锁
public <R,ID> R queryWithMutex(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbrFallback,Long time,TimeUnit unit){
String key= keyPrefix+id;
String shopJson= stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)){ //命中Redis缓存
return JSONUtil.toBean(shopJson, type);
}
if(shopJson!=null){ //命中空值缓存,代表恶意攻击
return null;
}
if (!tryLock(LOCK_SHOP_KEY+id)) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return queryWithMutex(keyPrefix,id,type,dbrFallback,time,unit);
}
String shopJson2= stringRedisTemplate.opsForValue().get(key);
//doubleCheck
if (StrUtil.isNotBlank(shopJson2)){ //命中Redis缓存
unlock(LOCK_SHOP_KEY+id);
return JSONUtil.toBean(shopJson2, type);
}
if(shopJson2!=null){ //命中空值缓存,代表恶意攻击
unlock(LOCK_SHOP_KEY+id);
return null;
}
R apply = dbrFallback.apply(id);
if (apply==null){ //生成空值缓存,避免缓存穿透
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES); //对象转Json
return null;
}
this.setWithLogicalExpire(key,JSONUtil.toJsonStr(apply),time,unit);
unlock(LOCK_SHOP_KEY+id);
return apply;
}
基于代码实现逻辑过期
public <R,ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
String key= CACHE_SHOP_KEY+id;
String Json= stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(Json)){ //未命中Redis缓存
return null;
}
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
return r;
}
if (tryLock(key)){
String json=stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)){ //命中Redis缓存 二次命中缓存,兜底
RedisData redisData1 = JSONUtil.toBean(json, RedisData.class);
if (redisData1.getExpireTime().isAfter(LocalDateTime.now())){
unlock(key);
return JSONUtil.toBean((JSONObject) redisData1.getData(), type);
}
}
CACHE_REBUILD_EXECUTOR.submit(()->{
R apply = dbFallback.apply(id);
this.setWithLogicalExpire(key,apply,time,unit);
unlock(key);
});
}
return r;
}
附工具类方法
public void set(String key, Object value, Long time, TimeUnit util){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,util);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit util){
RedisData redisData=new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(util.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
//线程池
public static final ExecutorService CACHE_REBUILD_EXECUTOR =
Executors.newFixedThreadPool(10);
/**
* 获取互斥锁
* @param key
* @return
*/
public Boolean tryLock(String key){
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
/**
* 释放互斥锁
* @param key
* @return
*/
public void unlock(String key){
stringRedisTemplate.delete(key);
}