redis


缓存穿透案例

比如我们查找一个店铺信息,按照正常的流程来讲

  1. 首先,从redis查找看能否找到店铺信息,如果找不到从数据库中查找,否则返回
  2. 从数据库中查找是,如果数据库不存在,报错。如果存在返回结果,并把查询到的信息放入redis中

但是如果一个用户,来查找一个不存在的id,这时我们每次都会查找数据库,如果是恶意的,该用户一直查询不存在数据,这样数据库压力会很大。

解决方法:

  1. 首先,从redis查找看能否找到店铺信息,如果redis命中的话,判断是否为空值,如果为空直接结束程序,如果不为空返回店铺信息
  2. 如果redis不存在,这时请求数据库,如果数据库也不存在,这时将空值写入redis,防止再次请求时同样以不存在的id,来查找。如果数据库中存在,返回店铺信息,并加入redis

缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

代码实现:


 /**
     * @Date: 2022/5/2 15:02
     * 原来写的解决缓存 穿透问题 解决方法
     */
 public Shop  queryWithPassThrough(Long id){
     //从redis查询缓存
     String key= RedisConstants.CACHE_SHOP_KEY +id;
     String shopJson = template.opsForValue().get(key);
     // 存在返回  当查到放入的空值时 也就是"" 不会进入下面这个if
     if(StrUtil.isNotBlank(shopJson)){
        return JSONUtil.toBean(shopJson, Shop.class);
     }
     //判断命中的是否是空值     当查到放入的空值时 也就是"" 会进入下面这个if
     if(shopJson!=null){
         //说明查到了放入的空值  
         return null;
     }
     //不存在   根据id  查数据库
     Shop byId = getById(id);
     //数据库不存在报错
     if(byId==null){
         //将 空值写入redis
         template.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
         return null;
     }
     //存在  返回并放入redis
     String s = JSONUtil.toJsonStr(byId);
     template.opsForValue().set(key,s,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
     return byId;
 }

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

给不同key加 不同的ttl 随机值 这种代码就不再演示了

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

1.互斥锁

2.逻辑过期

互斥锁代码实现

/**
 * @Date: 2022/5/2 15:00
 *模拟锁  来解决缓存击穿问题
 *
 */
private  boolean tryLock(String key){
    //使用setnx
    Boolean flag = template.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void  unlock(String key){
    template.delete(key);
}
public Shop queryWithMutex(Long id){
     String key=RedisConstants.CACHE_SHOP_KEY+id;
     String shopJson = template.opsForValue().get(key);
         // 存在返回    //判断命中的是否是空值     当查到放入的空值时 也就是"" 不会进入下面这个if
         //  这里是为了解决缓存穿透
         if(StrUtil.isNotBlank(shopJson)){
             return JSONUtil.toBean(shopJson, Shop.class);
         }
         //判断命中的是否是空值 当查到放入的空值时 也就是"" 会进入下面这个if
         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);
            //递归调用的过程中 可能拿到了别人新存入redis的key
            return queryWithMutex(id);
        }
        //成功  根据id查询数据库
        shop = getById(id);
        //模拟并发问题  同时高并发访问  只会查询一次数据库
        Thread.sleep(200);
        if(shop==null){
            template.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
             return null;
        }
        template.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);

    } catch (InterruptedException e) {
       throw new RuntimeException(e);
    } finally {
//一定要释放锁
        unlock(lockKey);
    }

    return shop;
}

逻辑过期代码实现

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
//定义线程池 
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

public Shop  queryWithLogicalExpire(Long id){
     //从redis查询缓存
     String key= RedisConstants.CACHE_SHOP_KEY +id;
     String shopJson = template.opsForValue().get(key);
     // 存在返回  未命中返回空
     if(StrUtil.isBlank(shopJson)){
         return null;
     }
     //命中   重点判断是否过期
     RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
     Object data = redisData.getData();
     Shop shop = JSONUtil.toBean((JSONObject) 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){
         //todo   成功 开启独立线程 实现缓存重建
         CACHE_REBUILD_EXECUTOR.submit(()->{
             try {
                 //测试时 设置为20  正常时候应该设为30分钟
                 this.saveShop2Redis(id,20L);
             } catch (Exception e) {
                 e.printStackTrace();
             }finally {
                 unlock(lockKey);
             }
         });
     }
     return shop;
 }



  public   void saveShop2Redis(Long id,Long expireSecoonds) throws InterruptedException {
        Shop shop = getById(id);
        //测试 延迟 模拟高并发场景
        Thread.sleep(200);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecoonds));
        template.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

通过id查询 缓存 一般不会存在未命中的情况 因为key是不会过期的 一般热点key会提前添加到缓存 所以不会存在是否会过期的问题,查不到的问题

一致性问题:一致性不太能保证


文章作者: 蛰伏
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 蛰伏 !
  目录