黑马点评第二章缓存篇(如何处理redis雪崩,穿透,击穿)
1,缓存临时存储数据的地方,读写性能较高
2,缓存更新策略
1)内存淘汰 不用自己维护,利用redis的内存淘汰机制,当内存不足时自动淘汰部分数据 下次查询更新缓存
2)超时剔除 给缓存数据据添加ttl时间,到期自动的删除缓存,下次查询时更新缓存
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
3)主动更新 编写业务逻辑,在修改数据库的同时,更新缓存
主要是根据业务场景的不同
低一致性需求,使用内存淘汰机制
高一致性需求:主动更新,并且超时剔除作为兜底,例如店铺详情的查询的缓存
3,主动更新的三个策略
1)有缓存的调用者,在更新数据库的同时进行更新缓存
2)缓存与数据整合为一个服务,由服务来维护一致性,调用者使用发i服务,无需关心缓存一致性的问题
3),调用者只操作缓存,有其他线程异步的将缓存数据持久化到数据库,保证最终一致性(最大的问题是在进行异步之前如果缓存发生宕机会导致数据丢失)
删除缓存还是更新缓存 一般来说是进行删除缓存,更新缓存,每次更新数据库都要进行更行缓存,无效写操作较多
如何缓存和数据库同时成功和失败
单体系统,将缓存,数据库操作放到同一个事务中
分布式系统,利用tcc等分布式事务方案
读操作
缓存命中直接返回
未命中查询数据库,并写入缓存,设定超时时间
写操作
一般先写数据库,然后删除缓存

以这张图片为参考,如果是先删除缓存再操作数据库 线程一删除缓存 但是同时线程二查询缓存未命中查询数据库写入缓存,此时线程一更新缓存 , 就是导致数据不一致
虽然先操作数据库再进行删除缓存也可以,但是更新数据库的时间比写入缓存的时间要长一点所以发生数据不一致性机会少一点
缓存穿透问题:
指的是客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库中
解决方案 1)缓存空对象 查询不到的时候,在redis中缓存null(对于比较占空间的时候可以进行设置一个ttl有效时间,但是可能造成短期不一致的情况)
2)布隆过滤 内存占用少,没有多余的key(但是实现复杂,可能存在误判)
3)增加id等索引的复杂度
4)加强用户权限校验
5)做好热点参数的限流
//模拟重建延时
if(shop==null){
//为空值,将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
缓存雪崩
指的是同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
1,给不同的key的ttl添加随机值
2,利用redis集群提高服务的可用性
3,给缓存业务添加降级限流策略
4,给业务添加多级缓存
缓存击穿问题
指的是热点key问题,就是高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会瞬间给数据库带来巨大的冲击
解决方案
1)互斥锁 其中用到redis中的方法setnx 就是允许一个key一个值不能发生改动
public Shop queryWithPassMutex(long id){//通过互斥锁来进行解决缓存击穿
String key=RedisConstants.CACHE_SHOP_KEY + id;
//从redis中进行查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//存在直接返回
Shop shop= JSONUtil.toBean(shopJson,Shop.class);
return shop;
}
//如果命中的是空值
if(shopJson!=null){
return null;
}
//不存在 实现缓存重建
//获取互斥锁
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
Shop shop= null;
try {
boolean isLock = tryLock(lockKey);
//判断是否成功
if(!isLock){
//获取失败
// 失败休眠并重试
Thread.sleep(50);
return queryWithPassMutex(id);
}
//成功,根据id查询数据库
shop = getById(id);
//模拟重建延时
if(shop==null){
//为空值,将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonPrettyStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//释放互斥锁
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
return shop;
}
2)逻辑过期
所谓逻辑过期就是在redis设置一个属性,每次调用者判断是否过期,如果没有过期直接使用,如果过期了,获取互斥锁,且重新开辟一个线程 进行缓存重建,但是原来的线程发挥的是原来的过期数据,其他线程没有获得锁后也会直接返回过期数据,这样做的话性能好一点,不会像互斥锁一样只有获取成功,其他线程还需要等待才能继续运行
//创建一个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(long id){
String key=RedisConstants.CACHE_SHOP_KEY + id;
//从redis中进行查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isBlank(shopJson)){
//不存在直接返回
return null;
}
//命中需要先把json反序列化成对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data =(JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime=redisData.getExpireTime();
//判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期 直接返回店铺信息
return shop;
}
//过期 需要进行缓存重建
//获取互斥锁
String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//判断是否获取成功
if(isLock){
//成功 开启独立线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unLock(key);
}
});
}
//不成功 返回过期的店铺信息
System.out.println(shop);
return shop;
}
更多推荐


所有评论(0)