缓存更新策略及穿透,雪崩,击穿的解决方案
文章目录
什么是缓存,缓存的概念
缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。
缓存的更新策略
业务场景: 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
总结
-
缓存更新策略的最佳实践方案:
-
低一致性需求:使用Redis自带的内存淘汰机制
-
高一致性需求:主动更新,并以超时剔除作为兜底方案
-
读操作: 缓存命中则直接返回 缓存未命中则查询数据库,并写入缓存,设定超时时间
-
写操作: 先写数据库,然后再删除缓存 要确保数据库与缓存操作的原子性
代码示例-主动更新策略
/**
* 事务控制双写
* @param shop
* @return
*/
@Override
@Transactional
public Result updateByShopId(Shop shop) {
if(shop.getId() == null){
return Result.fail("店铺不存在");
}
//更新数据库
updateById(shop);
//删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
return Result.ok();
}
缓存穿透
代码实现缓存穿透解决方案
private Shop queryWithPassThrough(Long id){
//去redis中查询
String key = CACHE_SHOP_KEY+id;
String shopCache = stringRedisTemplate.opsForValue().get(key);
Shop shop = new Shop();
//如果不为空则直接返回
if(StrUtil.isNotBlank(shopCache)){
shop = JSONUtil.toBean(shopCache, Shop.class);
return shop;
}
//判断命中的是否是空值
if(shopCache!=null){
return null;
}
//为空则去数据库查询
shop= getById(id);
//若数据库也没查到,redis中存放key值,避免缓存穿透问题
if(shop == null){
//放入空值
stringRedisTemplate.opsForValue().set(key,"");
//设置过期时间
stringRedisTemplate.expire(key, Duration.ofMinutes(CACHE_NULL_TTL));
return null;
}
//如果查到了则放入缓存中
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(key,shopStr);
return shop;
}
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
缓存null值
布隆过滤
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
第一个解决方案给不同 key值的过期时间加个随机值即可
注:涉及分布式,后期学了再更新
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
两种解决方案: 1-互斥锁 2-逻辑过期
互斥锁与逻辑过期
互斥锁解决方案代码案例
private Shop queryWithMutex(Long id){
//1-去redis中查询商铺信息
String key = CACHE_SHOP_KEY+id;
String shopCache = stringRedisTemplate.opsForValue().get(key);
Shop shop = new Shop();
//2-如果不为空则直接返回
if(StrUtil.isNotBlank(shopCache)){
shop = JSONUtil.toBean(shopCache, Shop.class);
return shop;
}
//3-判断命中的是否是空格即 空格
if(shopCache != null){
//是空值 则直接拦截
return null;
}
//4 -不是空值- 需要缓存重建
//5-缓存重建
//5.1-获取互斥锁
String lockKey =LOCK_SHOP_KEY + id;
try {
boolean flag = tryLock(lockKey);
//5.2- 判断是否获取成功
if(!flag){
//5.3 - 失败则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//5.4 - 成功 查询数据库写入缓存
shop= getById(id);
//模拟延迟场景 延长重建时间
Thread.sleep(200);
//5.5 - 若数据库也没查到,redis中存放key值及空值,避免缓存穿透问题
if(shop == null){
//放入空值
stringRedisTemplate.opsForValue().set(key,"");
//设置过期时间
stringRedisTemplate.expire(key, Duration.ofMinutes(CACHE_NULL_TTL));
return null;
}
//5.5 - 如果查到了则放入缓存中
String shopStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(key,shopStr);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放互斥锁
unlock(lockKey);
}
return shop;
}
代码用到的新建互斥锁方法
//加锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放
private void unlock(String key){
stringRedisTemplate.delete(key);
}
逻辑过期方案
逻辑过期需要用到线程,这里用线程池创建
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
需要先往redis中放入一个热点key 设置好逻辑过期时间,用来模拟
public void saveShop2Redis(Long id ,Long expireSeconds) throws InterruptedException {
//查询店铺数据
Shop shop = getById(id);
Thread.sleep(200);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
//单元测试代码
@Resource
ShopServiceImpl shopService;
@Test
void saveShop2Redis() throws InterruptedException {
shopService.saveShop2Redis(1L,10L);
}
代码实现-逻辑过期的方式解决缓存击穿
private Shop queryWithLogicExpire(Long id){
//1-去redis中查询
String key = CACHE_SHOP_KEY+id;
String shopCache = stringRedisTemplate.opsForValue().get(key);
//2 如果未命中 则直接返回
if(StrUtil.isBlank(shopCache)){
return null;
}
//3 命中,先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopCache, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//获取过期时间
LocalDateTime expireTime = redisData.getExpireTime();
//4 判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期 直接返回店铺信息
return shop;
}
//5 已过期 需要缓存重建
//5.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY+id;
boolean isLock = tryLock(lockKey);
//5.2 判断是否获取成功
if(isLock){
//5.3 成功 开启独立线程 实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
//5.4 缓存重建
try {
saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unlock(lockKey);
}
});
}
//5.5 返回过期对象
return shop;
}
@Override
public Result queryShopById(Long id) {
//缓存穿透
// Shop shop = queryWithPassThrough(id);
//缓存击穿 互斥锁
// Shop shop = queryWithMutex(id);
//逻辑过期来解决缓存击穿
Shop shop = queryWithLogicExpire(id);
if(shop == null){
return Result.fail("店铺信息不存在");
}
return Result.ok(shop);
}