缓存穿透:大量请求访问不存在的资源(既不存在于数据库,也不存在Redis),这将导致每次请求都要到数据库去查询,可能导致数据库挂掉。

解决方法
1. 直接对空值进行缓存

优点:实现简单,维护方便。缺点:额外内存消耗,造成短期数据不一致。
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
2. 使用布隆过滤器

原理:利用多个哈希函数将元素映射到一定长度的位数组中的多个位置,将相应位置位数组初始值0改为1。当判断一个元素是否存在于布隆过滤器中,只需要将该元素进行多次哈希,检查对应位数组上的值是否为1。当所有对应位都为1时,则该元素存在。(误判:查询时不同元素对应位置的1可能组成不存在的元素,一般控制在5%的误判率,误判率可以通过扩大数组长度来缓解)
基于策略模式和工厂模式实现的布隆过滤器可参见基于策略模式和工厂模式实现布隆过滤器-优快云博客
实现布隆过滤器的方式还有很多,Java实现布隆过滤器的几种方式总结_java_脚本之家。
缓存雪崩:指redis中同一时间大量的key集体过期导致请求全部转发到数据库,数据库瞬时压力过重导致雪崩。
解决方式:增加缓存时间的随机性,例如在原有的失效时间上增加一个随机值。
缓存击穿:某个热点key过期瞬间有大量用户访问该过期key或者高并发下该数据只在数据库而不在缓存中,这时候大并发请求会瞬间把数据库压垮。
解决方法:
1. 使用互斥锁。只有一个请求可以获取互斥锁,将数据库中查询到的数据加入到缓存,此时未获得锁的请求可以休眠部分时间再重新尝试获得锁。

可直接使用的工具类(利用redis中的setnx实现锁)
private final StringRedisTemplate stringRedisTemplate;
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R>type, Function<ID, R> dbFallback,Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1. 从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
// 2. 存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if(json != null){
return null;
}
// 不存在
R r = dbFallback.apply(id);
if (r == null){
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
// 存在
this.set(key, r, time, unit);
return r;
}
2.使用逻辑过期。
需要注意的是:在程序执行前要先对热点数据进行缓存预热,其次是获得锁的线程并不会去执行缓存更新,而是开启一个新线程,自己和当前未获得锁的其他线程一样返回过期数据。

模拟热点数据预热:
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
public void saveShop2Redis(Long id, Long TTL) {
// 1. 查询店铺数据
Shop shop = getById(id);
// 2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusMinutes(TTL));
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 3.写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
}
逻辑过期可直接使用的工具类:
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1. 从redis查询缓存
String s = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(s)) {
return null;
}
// 命中
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
// 未过期 直接返回
return r;
}
// 已过期
String lockkey = LOCK_SHOP_KEY + id;
if(tryLock(lockkey)){
try {
CACHE_REBUILD_EXECUTOR.submit(() -> {
R r1 = dbFallback.apply(id);
this.setWithLogicalExpire(key, r1, time, unit);
});
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
unLock(lockkey);
}
}
return r;
}

420

被折叠的 条评论
为什么被折叠?



