- 写工具类时,不确定的数据类型以及实体类要善于用泛型表示,由调用者来告诉我们真实的类型是什么,从而做出泛型的推断
- 涉及到数据库查询的时候可以使用函数式编程,还是由调用者来告诉我们数据库查询的方式,那数据库查询也是一个有参数和返回值的函数所以可以使用Function<T,R>指定参数和返回值类型,在调用者调用的时候指定即可
//调用者
Shop shop = cacheClient.queryWithLogicExpire(
RedisConstants.CACHE_SHOP_KEY,
id,
20L,
TimeUnit.SECONDS,
Shop.class,
this::getById); 数据库执行的函数 有一个参数和一个返回值
public <R,ID> R queryWithLogicExpire(String keyPrefix, ID id, Long expireTime, TimeUnit timeUnit, Class<R> type, Function<ID,R> databaseFallBack) //ID,R 参数类型,返回值类型
{
....
R selectR = databaseFallBack.apply(id);
....
}
更通俗一点就是 可以把调用者的 this::getById 替换成
ID -> this.getById(ID);
1.缓存穿透:
指缓存和数据库中都没有的数据,而用户不断发起请 求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
2. 缓存击穿
某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进
来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬
时数据库请求量大、压力骤增,甚至可能打垮数据库。
课程中学习到了可以通过互斥锁和对空值缓存同时解决两者的问题
public Shop queryWithPassMutex(Long id){
//1.从redis查询商户缓存
// Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(RedisConstants.CACHE_SHOP_KEY + id);
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopString = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopString)) {
//3.存在
return JSONUtil.toBean(shopString, Shop.class);
}
//判断空值
if (shopString != null){
//是空字符串
return null;
}
Shop shop = null;
String lockKey = RedisConstants.LOCK_SHOP_KEY +id;
try {
if (!tryLock(lockKey)) {
//获取锁失败 重复递归执行,获取缓存信息
System.out.println("-------------Lock Fail");
Thread.sleep(300);
return queryWithPassMutex(id);
}
System.out.println("-----------------Lock Success");
//4.数据库查询
LambdaQueryWrapper<Shop> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Shop::getId,id);
shop = shopMapper.selectOne(queryWrapper);
//模拟重建延时
Thread.sleep(200);
if (shop==null) {
//同时解决缓存穿透,这个数据在数据库中也不存在,防止恶意攻击
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//查询有结果 写入redis中
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放互斥锁
loseLock(lockKey);
}
return shop;
}
对于缓存击穿还有通过设置逻辑过期时间的方法来解决
//创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//缓存击穿 通过设置逻辑过期时间
public Shop queryWithLogicExpire(Long id,Long expireTime){
//1.从redis查询商户缓存
// Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(RedisConstants.CACHE_SHOP_KEY + id);
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJsonInfo = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isBlank(shopJsonInfo)) {
//3.未命中缓存
return null;
}
//命中 JSON反序列化
RedisData redisData = JSONUtil.toBean(shopJsonInfo, RedisData.class);
//类型为OBject 所以要先转为JSONObject
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime dataExpireTime = redisData.getExpireTime();
//判断是否过期 过期:缓存重建 未过期:返回店铺信息
if (dataExpireTime.isAfter(LocalDateTime.now())){
log.info("未过期");
return shop;
}
//缓存重建 "expireTime": 1691030052865
//获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
//判断是否拿到锁
//true:开启线程 实现缓存重建 false:返回旧的店铺信息
if (tryLock(lockKey)){
log.info("已过期");
CACHE_REBUILD_EXECUTOR.submit( () ->{
try {
//重建
this.saveShop2Redis(id, expireTime);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
loseLock(lockKey);
}
});
}
return shop;
}