Redis缓存的问题都是因为缓存过期,导致大量请求打到数据库,给数据库添加了压力。
以下是典型的三个缓存问题。
缓存穿透
概念
缓存穿透: 频繁请求缓存和数据库中没有的数据,导致数据库的压力过大
解决方案
- 规则校验:增加对key的规则校验,防止恶意请求
- 设默认值:数据库中没有数据时,给该key一个默认值,并设置合理的过期时间,防止较长时间的数据不一致
- 布隆过滤器:布隆过滤器中存在该key就通过,不存在就不访问Redis和数据库
基于设空值代码实现
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit){
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 如果缓存中有数据且不为空,直接返回
if (StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json, type);
}
// 如果缓存中的数据为空,返回空值
if ("".equals(json)){
return null;
}
// 没有缓存,查询数据库
R result = doFallback.apply(id);
// 如果从数据库中查询不到数据,设空值
if (result == null){
stringRedisTemplate.opsForValue().set(key, "", time, timeUnit);
return null;
}
// 数据库中有数据
this.set(key, result, time, timeUnit);
return result;
}
缓存雪崩
概念
大量key同时过期或者Redis宕机,同时,大量请求打过来,导致数据库压力突然增大
解决方案
- 如果是宕机导致的雪崩,配置Redis集群,提高可用性
- 设置均匀分布的过期时间,防止同时过期,比如在5分钟的基础上,加上0-180秒的随机值
- 使用限流策略
- 服务降级,当Redis不可用时,进行服务降级,或者停用部分服务
缓存击穿
概念
大量请求同时访问一个过期的热key,导致数据库压力突然增大
解决方案
- 使用互斥锁:大量请求进来,只有一个请求能够拿到锁访问数据库,减少不必要的访问
- 设置永不过期:给热key不设置过期时间,或者开一个异步线程定时去更新热key的过期时间。
使用互斥锁解决
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 设置逻辑过期
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
return BooleanUtil.isTrue(flag);
}
public void unlock(String key){
stringRedisTemplate.delete(key);
}
基于JVM的synchronized实现缓存击穿
// 基于synchronized互斥锁解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit) {
// redis键
String key = keyPrefix + id;
// 从redis中查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否有缓存 防止缓存穿透
if (StrUtil.isBlank(json)){
// 不存在
return null;
}
// 存在
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R result = JSONUtil.toBean(data, type);
// 判断是否过期
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())){
// 未过期,直接返回
return result;
}
// 缓存过期,需要缓存重建
String lockKey = "lock:" + id;
// 获取互斥锁
// 减小锁的粒度,锁的是每个键
synchronized (lockKey.intern()){
// 由于可能在这之前有人拿到锁并修改了,防止重复修改
// 双重检查锁
// 判断是否命中
// 1.从redis中查询缓存
String json2 = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json2)){
// 不存在
return null;
}
// 存在
RedisData redisData2 = JSONUtil.toBean(json, RedisData.class);
JSONObject data2 = (JSONObject) redisData2.getData();
R result2 = JSONUtil.toBean(data2, type);
// 判断是否过期
LocalDateTime expireTime2 = redisData2.getExpireTime();
if (expireTime2.isAfter(LocalDateTime.now())){
// 未过期,直接返回
return result;
}
// 过期了,去数据库中查询新数据
R apply = doFallback.apply(id);
// 存入redis
this.setWithLogicalExpire(key, apply, time, timeUnit)
}
// 返回旧值
return result;
}
这种方式可以解决穿透问题,不过效率较低,会阻塞等待获取锁,降低了吞吐量。
基于Redis的SetNX实现
同时,我们可以根据redis的setnx的原子性来优化。
// 基于setnx互斥锁解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> doFallback, Long time, TimeUnit timeUnit) {
// redis键
String key = keyPrefix + id;
// 从redis中查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否有缓存 防止缓存穿透
if (StrUtil.isBlank(json)){
// 不存在
return null;
}
// 存在
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R result = JSONUtil.toBean(data, type);
// 判断是否过期
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())){
// 未过期,直接返回
return result;
}
// 缓存过期,需要缓存重建
String lockKey = "lock:" + id;
// 尝试获取锁
boolean isLock = tryLock(lockKey);
if (isLock){
// 由于可能在这之前有人拿到锁并修改了,防止重复修改
// 双重检查锁
// 判断是否命中
// 1.从redis中查询缓存
String json2 = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json2)){
// 不存在
return null;
}
// 存在
RedisData redisData2 = JSONUtil.toBean(json, RedisData.class);
JSONObject data2 = (JSONObject) redisData2.getData();
R result2 = JSONUtil.toBean(data2, type);
// 判断是否过期
LocalDateTime expireTime2 = redisData2.getExpireTime();
if (expireTime2.isAfter(LocalDateTime.now())){
// 未过期,直接返回
return result2;
}
// 重建缓存
try {
// 查询数据库
R r1 = doFallback.apply(id);
// 写入redis
this.setWithLogicalExpire(key, r1, time, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
}
// 返回旧值
return result;
}
如果获取锁失败,就直接返回上次的旧值,这种方式的效率较高,提高了系统的吞吐量,但是存在数据不一致的问题。
我们还可以对上述过程进行优化,将查询数据库和释放锁的过程交给一个新线程去做。
// 重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
// 重建缓存
try {
// 查询数据库
R r1 = doFallback.apply(id);
// 写入redis
this.setWithLogicalExpire(key, r1, time, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
基于Redis的SetNX实现虽然效率较高,但是存在一个极端问题,在释放锁的过程中,有可能会误删他人的锁,因为释放锁的过程不是原子性的。
在线程1释放锁的过程中,恰好线程1cpu时间片用完,同时这个时候线程2拿到了锁,然后线程2cpu时间片用完,但是任务还没执行完,同时线程1得到新的时间片开始执行释放锁,这个时候就会把线程2的锁误删。
标签:缓存,return,String,过期,Redis,兄弟,json,key From: https://blog.csdn.net/z2698751935/article/details/137371839