缓存工具类

​ 在解决缓存技术中存在的问题时,其解决方案基本上是有固定套路的

​ 针对缓存穿透问题(指查询数据既不存在于缓存中,也不存在于数据库中,如果有用户恶意刷该请求,会给数据库造成巨大压力,影响系统性能),有以下两种解决方案:

  1. 当数据库查询不到数据时,给缓存返回空值。这个方案也是有问题的,比如此后不久数据库中插入了该数据,那么直到缓存中的空值过期,否则用户一直无法取到对应数据,并且会消耗额外的内存。
  2. 采用布隆过滤器技术:这是一种利用二进制来判断缓存中是否有对应数据的技术,作用于用户请求和Redis之间,能够过滤掉缓存和数据库中都不存在数据的请求。但是实现复杂,并且存在误判可能(类似于哈希冲突)

​ 针对缓存雪崩问题(指某一段时间,缓存中的大量数据失效在项目初始化时,有大量数据同时加入缓存,又称为数据预热或者缓存服务挂了,导致大量请求涌入数据库,给数据库造成巨大压力,影响系统性能),有以下解决方案

  1. 给缓存中的每条数据设置随机的过期时间。该方案只能解决前一种情况
  2. 针对第二种情况,可以采用缓存集群以及设置多级缓存的方式加以解决。

​ 针对缓存击穿问题(通常是针对某些热点key,比如某些促销活动,在很短时间内有大量请求涌入,此时某个key失效,会导致大量请求不断涌入数据库,给数据库造成巨大压力,不仅影响用户体验,还可能使系统崩溃),有以下解决方案:

  1. 给缓存中的数据设置逻辑过期时间,即缓存中的每条数据都不会失效,而是让程序员在编写代码时判断该数据是否有效。这种做法的优点是性能较高,但是由于缓存中的数据不会失效,因此保证不了数据一致以及消耗了额外内存。
  2. 设置互斥锁,即当第一个线程发现缓存中无数据时,它会去访问数据库同时给该数据上锁,上锁的目的是让其他线程无法访问缓存从而也引发缓存重构。这种做法的优点是能够保证数据的一致性,缺点是由于线程必须等待,因此性能较低

从上面这几种缓存中出现的问题,我们可以得出缓存中的问题基本上都是关于数据不一致的以及数据库压力过大的问题,由于使用场景颇多,如果每次开发时,都要额外写一套代码,既繁琐代码也没得到复用,基于以上情况,开发一个缓存工具类,具有以下功能。

  1. 可以将任意java对象存储进String类型的value中,并可以设置有效时间

    1
    2
    3
    public void set(String key, Object value, Long time, TimeUnit timeUnit){
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time,timeUnit);
    }
  2. 可以将任意java对象存储进String类型的value中,并可以设置逻辑过期时间,用于处理缓存击穿问题

    1
    2
    3
    4
    5
    6
    7
    public void setWithLogic(String key,Object value,Long time,TimeUnit timeUnit){
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));

    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    这里针对逻辑过期的做法是对数据进行再封装,符合OCP原则。

  3. 根据key查询数据并反序列化为指定类型,当无该数据时将空值存储进redis中用于解决缓存穿透问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 根据key查询数据并反序列化为指定类型,当无该数据时将空值存储进redis中用于解决缓存穿透问题
    * @param prefix 前缀和id组合在一起构成key
    * @param id
    * @param dbFeedBack 由调用者指定查询数据库的逻辑,是查询商户还是其他逻辑
    * @param time
    * @param timeUnit
    * @param type 返回值类型
    * @return
    * @param <R> 由调用者指定id类型
    * @param <ID> 由调用者指定返回值类型
    */
    public <R,ID>R queryWithPassThrough(String prefix, ID id, Class<R>type,Function<ID,R> dbFeedBack,Long time,TimeUnit timeUnit){
    String key = prefix + id;

    String json = stringRedisTemplate.opsForValue().get(key);
    if(StrUtil.isNotBlank(json)){
    return JSONUtil.toBean(json, type);
    }

    if(json != null){
    return null;
    }

    R ret = dbFeedBack.apply(id);
    if(ret == null){
    set(key,"",time,timeUnit);
    }else{
    set(key,ret,time,timeUnit);
    }

    return ret;
    }
  4. 根据key查询数据并反序列化为指定类型,并使用逻辑过期解决缓存击穿问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public <R,ID>R queryWithLogicalExpire(String prefix,ID id,Class<R>type,Function<ID,R>dbFeedBack,Long time,TimeUnit timeUnit){
    String key = prefix + id;
    //查看Redis中是否有该商户
    String json = stringRedisTemplate.opsForValue().get(key);
    if(StrUtil.isNotBlank(json)){
    //Redis中有该商户,直接返回
    R cacheData = JSONUtil.toBean(json,type);
    return cacheData;
    }
    if(json != null){
    //shopJson是空字符串,说明数据库中也没有该店铺
    return null;
    }
    //开始缓存重建
    R ret = null;
    String lockKey = LOCK_SHOP_KEY + id;
    try {
    boolean lock = getLock(lockKey);
    if(!lock){
    //有其他线程在重建,进入等待
    Thread.sleep(50);
    return queryWithLogicalExpire(prefix,id,type,dbFeedBack,time,timeUnit);
    }
    ret = dbFeedBack.apply(id);
    if (ret == null) {
    //数据库中也没有该数据,查询错误,向Redis中插入空数据防止缓存穿透,并设置2min有效期
    set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
    }else{
    setWithLogic(key,ret,CACHE_SHOP_TTL,TimeUnit.MINUTES);
    }
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    } finally {
    delLock(lockKey);
    }
    return ret;
    }

    //设置互斥锁防止缓存击穿,设置锁的有效期为10s,防止发生故障锁未释放
    public boolean getLock(String key){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "0", 10L, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
    }
    //释放锁
    public void delLock(String key){
    stringRedisTemplate.delete(key);
    }

    通过本节需要掌握以下知识点:

    1. 如何通过互斥锁来解决缓存击穿问题的,互斥锁是如何实现的
    2. 在编写该工具类时泛型的使用以及Function是如何使用的