目录
缓存更新策略
异常情况发生的概率 1.先删缓存,在操作数据库 > 2. 先操作数据库,在删缓存
缓存穿透
缓存雪崩
缓存击穿
逻辑过期:在缓存中单独设置过期时间字段,在检测到过期后,获取锁开一个新线程去更新数据,自己返回老的数据,其他请求发现过期,会尝试获取锁,发现获取不到则已经有人在更新,自己则依然返回老的数据。
互斥锁:
使用setNX模拟互斥锁,设置成功则继续运行code,失败则等待继续尝试从缓存中获取数据
@Component
public class RedisUtil {
@Autowired
StringRedisTemplate stringRedisTemplate;
// 使用redis模拟锁lock的功能
public Boolean tryLock(String key){
return stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
}
// 使用redis模拟锁lock的功能
public Boolean unLock(String key){
return stringRedisTemplate.delete(key);
}
}
逻辑过期
//给缓存数据添加过期时间
public void saveRedisByExpire(User user, int expireSeconds) {
RedisData data = new RedisData();
data.setData(user);
data.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
stringRedisTemplate.opsForValue().set("user:1", JSONUtil.toJsonStr(data));
}
//逻辑过期的代码
private static final ExecutorService CACHE_REBUID_EXECUTOR = Executors.newFixedThreadPool(10);
User queryWithLogicExpire() throws JsonProcessingException, InterruptedException {
String key = "user:" + 1;
//1.从redis查询缓存
String result = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在 防止缓存穿透(查询redis和数据库中都不存在的数据,当数据不存在时,设置value=""的数据,并设置过期时间,
// 在过期时间内,可能存在数据库和缓存数据不一致的风险,所以设置的过期时间不宜过长)
if (StringUtils.isBlank(result)) {
//3.存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData data = JSONUtil.toBean(result, RedisData.class);
User user = JSONUtil.toBean((JSONObject) data.getData(), User.class);
LocalDateTime expireTime = data.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1未过期,直接返回
return user;
}
//5.2已过期,需要缓存重建
//6.重建缓存
//6.1获取互斥锁
String lockKey = "lock:user:" + 1;
//6.2判断是否获取锁成功
if (redisUtil.tryLock(lockKey)) {
//6.3成功,开启一个线程实现缓存重建
CACHE_REBUID_EXECUTOR.submit(() -> {
//模拟查询数据库
User userDB = new User("wangwu", 54);
try {
redisUtil.saveRedisByExpire(userDB, 100);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("存储Redis错误", e);
} finally {
redisUtil.unLock(lockKey);
}
});
}
//6.4 主线程结束,spring中redisUtil会销毁,则报连接异常,真实环境不存在这种情况
Thread.sleep(3000);
return user;
}
Redis 工具类封装
package com.lb.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.lb.pojo.RedisData;
import com.lb.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Component
public class RedisCacheUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final Long CACHE_NULL_TTL = 5L;
private static final Long CACHE_USER_TTL = 30L;
public void set(String key, Object value, Long time, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
}
//处理缓存击穿
public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit) {
//设置逻辑过期
RedisData data = new RedisData();
data.setData(value);
data.setExpireTime(LocalDateTime.now());
//写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), time, timeUnit);
}
private static final ExecutorService CACHE_REBUID_EXECUTOR = Executors.newFixedThreadPool(10);
//缓存击透的查询
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFunction, Long time, TimeUnit timeUnit) {
String key = keyPrefix + id;
//1.从redis查询缓存
String result = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在 防止缓存穿透(查询redis和数据库中都不存在的数据,当数据不存在时,设置value=""的数据,并设置过期时间,
// 在过期时间内,可能存在数据库和缓存数据不一致的风险,所以设置的过期时间不宜过长)
if (StrUtil.isNotBlank(result)) {
//3.存在,直接返回
return JSONUtil.toBean(result, clazz);
}
//数据为空字符串时
if (result != null) {
//返回一个错误信息
return null;
}
//4.不存在,根据id查询数据库
R r = dbFunction.apply(id);
//5.不存在,返回错误
if (r == null) {
//数据库不存在,防止缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入Redis
setWithLogicExpire(key, r, time, timeUnit);
return r;
}
// 使用redis模拟锁lock的功能
public Boolean tryLock(String key) {
return stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
}
// 使用redis模拟锁lock的功能
public Boolean unLock(String key) {
return stringRedisTemplate.delete(key);
}
//缓存过期的查询
public <R, ID> R queryWithLogicExpire(
String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFunction, Long time, TimeUnit timeUnit) {
String key = keyPrefix + id;
//1.从redis查询缓存
String result = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isBlank(result)) {
//3.存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData data = JSONUtil.toBean(result, RedisData.class);
R r = JSONUtil.toBean((JSONObject) data.getData(), clazz);
LocalDateTime expireTime = data.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.1未过期,直接返回
return r;
}
//5.2已过期,需要缓存重建
//6.重建缓存
//6.1获取互斥锁
String lockKey = "lock:user:" + 1;
//6.2判断是否获取锁成功
if (tryLock(lockKey)) {
//6.3成功,开启一个线程实现缓存重建
CACHE_REBUID_EXECUTOR.submit(() -> {
try {
//查询数据库
R r1 = dbFunction.apply(id);
//写入数据库
setWithLogicExpire(key, id, time, timeUnit);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("存储Redis错误", e);
} finally {
unLock(lockKey);
}
});
}
return r;
}
private void queryWithPassThrough_Test() {
String CACHE_USER_KEY = "user:";
RedisCacheUtil util = new RedisCacheUtil();
User user = util.queryWithPassThrough(CACHE_USER_KEY, 1L, User.class, this::getById, CACHE_USER_TTL, TimeUnit.MINUTES);
}
private User getById(Long id) {
//模拟查询数据库
return new User("lisi", 31);
}
}
布隆过滤器
方式一 : 基于google的guava
方式二:基于redission
Redis - Redisson_naki_bb的博客-CSDN博客_redisson redis