什么是缓存
缓存就是数据交换的缓冲区,是临时储存数据的地方,读写性能高。
项目使用场景
在项目中,我们一般把读写频繁的数据缓存到redis中,以减少数据库的压力,降低后台的负载,提高读写效率,减少响应时间
在项目中使用缓存
黑马的点评项目:
对于项目中的获取店铺列表是经常需要请求的,所以我们就以此为例:
缓存店铺列表
1.首先在redis中查找,判断是否命中
命中,直接返回具体信息
未命中,查找数据库,将数据保存至redis中一份,同时返回给前端具体信息
缓存更新策略
在项目中,如果我们直接操作了数据库,从而导致数据库中数据和redis中数据不一致,这种问题该如何解决呢?
1.采用redis自己的内存淘汰机制,让他在内存占用多时,自己选择淘汰一部分,下次查询再更新。
2.给redis中键设置过期时间
3.程序员在对数据库操作时,同时更新redis
折中一下,给键设置过期时间,如果过期时间短的话,出现错误的概率会很小,也容易操作
对于数据的更新,我们是先删缓存再操作数据库呢?,还是先操作数据库,再删除缓存呢?
在高并发环境下,由于操作数据库的时间肯定是比redis操作时间消耗大的,所以,我们为了减少错误,应该先进行数据库的更新,再进行redis中的操作
缓存穿透
次数一多,这会导致数据库的压力变大
解决方案:
1.缓存空对象
第一次如果未命中,将其缓存到redis中,值为空,这样下一次还请求,就直接redis返回空
优点:简单,容易实现
缺点:造成内存浪费,可能造成短期不一致(我存了个null,但刚好我数据库又多了一个这样的真实值)
2.布隆过滤
请求过来,先过布隆过滤,检查数据是否存在,如果不存在,直接拒绝
优点:消耗少,没有多余key
缺点:实现复杂,存在误判
在代码中实现:
//将list按String存储
@Override
public List<ShopType> getShopType() {
String key = SHOP_TYPE_LIST;
// 首先判断redis中是否有数据,是否命中
String s = stringRedisTemplate.opsForValue().get(key);
if (!StrUtil.isBlank(s)) {//不为空
// 将String转成list
List<ShopType> shopTypes = JSONUtil.toList(s, ShopType.class);
return shopTypes;
}
// 判断命中是否是空值
if (s != null) {//经过上面判断,不是null,就一定是空字符串
return null;
}
// 获取成功,查找数据库
List<ShopType> typeList= this.query().orderByAsc("sort").list();
if (typeList == null) {
// 存储空对象
stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, "", SHOP_TYPE_NULL, TimeUnit.MINUTES);
return typeList;
}
// 保存到redis
String type = JSONUtil.toJsonStr(typeList);
stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, type, SHOP_TYPE_TIME, TimeUnit.MINUTES);
return typeList;
}
缓存雪崩
解决方案:
缓存击穿
解决方案:
1.通过互斥锁
让一个线程得到锁,进行缓存创建,其余等待
代码:
@Override
public List<ShopType> getShopType() {
String key = SHOP_TYPE_LIST;
// 首先判断redis中是否有数据,是否命中
String s = stringRedisTemplate.opsForValue().get(key);
if (!StrUtil.isBlank(s)) {//不为空
// 将String转成list
List<ShopType> shopTypes = JSONUtil.toList(s, ShopType.class);
return shopTypes;
}
// 判断命中是否是空值
if (s != null) {//经过上面判断,不是null,就一定是空字符串
return null;
}
String lockKey = SHOP_TYPE_LOCK;
List<ShopType> typeList = null;
// 获取lock
try {
boolean b = tryLock(lockKey);
if (!b) {
// 获取失败,休眠,递归尝试再次获取
Thread.sleep(50);
return getShopType();
}
// 获取成功,查找数据库
typeList= this.query().orderByAsc("sort").list();
if (typeList == null) {
// 存储空对象
stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, "", SHOP_TYPE_NULL, TimeUnit.MINUTES);
return typeList;
}
// 保存到redis
String type = JSONUtil.toJsonStr(typeList);
stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, type, SHOP_TYPE_TIME, TimeUnit.MINUTES);
}catch (Exception e){
return null;
}finally {
// 释放锁
unlock(SHOP_TYPE_LOCK);
}
return typeList;
}
//尝试获取锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//相当于setnx,当值存在时,不会进行修改
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
2.逻辑过期
永久保存该值,过期时间不设置,而是在保存对象中设置一个过期时间标识,让程序判断是否过期,采用互斥锁,让一个线程再开辟一个线程用作缓存创建,返回数据返回原有逻辑过期数据即可
代码:
// 线程池
private static final ExecutorService CACHE_REBULD = Executors
.newFixedThreadPool(10);
// 逻辑删除解决缓存击穿
public List<ShopType> getShopType2() {
// 1.从redis中查询缓存
String key = SHOP_TYPE_LIST;
String lockKey = SHOP_TYPE_LOCK;
// 2.判断是否存在
// 首先判断redis中是否有数据,是否命中
String s = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(s)) {//为空,直接返回null
System.out.println("为空");
return null;
}
// 3.命中,将redis中redisData对象反序列化出来
RedisData redisData = JSONUtil.toBean(s, RedisData.class);
// 拿到用户信息
List<ShopType> shopTypes = JSONUtil.toList((JSONArray)redisData.getData(), ShopType.class);
// 拿到过期时间
LocalDateTime expireTime = redisData.getExpireTime();
// 4.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){//是否在当前时间之后
// 之后说明已经没有过期过期
System.out.println("未过期");
// 未过期,直接返回相应信息
return shopTypes;
}
// 5.过期,缓存重建
// 获取互斥锁
boolean b = tryLock(lockKey);
System.out.println(b);
// 判断是否获取成功
if(b){
// 成功开启独立线程进行缓存重建
CACHE_REBULD.submit(()->{
try {
System.out.println("开始重建缓存");
this.sendSomeExample();
}catch (Exception e){
throw new RuntimeException("逻辑删除新线程的缓存重建失败");
}finally {
// 释放锁
this.unlock(lockKey);
}
});
}
// 获取失败返回过期对象
return shopTypes;
}
优缺点: