美文网首页
还在为缓存头疼?来看看更优雅的缓存方案(二)

还在为缓存头疼?来看看更优雅的缓存方案(二)

作者: Felix_ | 来源:发表于2021-06-11 11:12 被阅读0次

上一节还在为缓存头疼?来看看更优雅的缓存方案(一)中,搭建了一个简单的项目,介绍了SpringCache几个常用的注解,回顾一下:

  • @Cacheable 缓存中没有则加入缓存,有则读取缓存
  • @CachePut 缓存中没有则加入,有则强制更新
  • @CacheEvit 从缓存中删除数据
  • @Caching 组合操作

其中@CacheEvit 的删除操作,在实际应用中,我们会碰到一个问题,就是有@CacheEvit的函数,那么它是在函数执行之前还是之后去删除缓存呢?我们是否可以进行控制?来看一下源码


  // IntelliJ API Decompiler stub source generated from a class file
  // Implementation of methods is not available

package org.springframework.cache.annotation;

@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Inherited
@java.lang.annotation.Documented
public @interface CacheEvict {
    @org.springframework.core.annotation.AliasFor("cacheNames")
    java.lang.String[] value() default {};

    @org.springframework.core.annotation.AliasFor("value")
    java.lang.String[] cacheNames() default {};

    java.lang.String key() default "";

    java.lang.String keyGenerator() default "";

    java.lang.String cacheManager() default "";

    java.lang.String cacheResolver() default "";

    java.lang.String condition() default "";

    boolean allEntries() default false;

    boolean beforeInvocation() default false;
}

可以看到,其中有一个beforeInvocation()的布尔类型,默认为false,意思就是说@CacheEvit 默认在函数执行后进行缓存的删除,所以我们可以根据业务需要,选择在函数执行前/后删除缓存中的数据.
接着上节的内容来说,使用redis经常碰到的缓存穿透缓存雪崩缓存击穿究竟如何解决呢?先来看下他们的定义

  • 缓存穿透

客户端大量/频繁查询一个/多个不存在的key,导致业务直接穿过缓存到达数据库

  • 缓存雪崩

大量key同时过期,导致同一时间内请求都直接到数据库去取数据

  • 缓存击穿

热点key过期,同一时间大量针对该key的请求频繁到达数据库层


那么以上常见的三种问题解决方案分别是什么呢?


  • 缓存穿透

对于空值进行缓存,即使从数据库查到的数据为空,那么也进行缓存,这样,即使大量/频繁的查询一个/多个key,也能直接命中缓存并且返回给前端

  • 缓存雪崩

不同的key设置不同的或者随机的过期时间,避免key在相同的时间大量过期.当然有时候可能弄巧成拙,比如采用随机的过期时间 + 最小过期时间 = 过期时间,那么由于业务发生的时间本身就是随机的,所以计算出来的过期时间就有可能为相同的值.所以,最终只要给key设置过期时间即可

  • 缓存击穿

对于非强一致的数据,在单个微服务中加锁进行管控,在热点key过期的时候进行加锁,只允许第一个请求到达数据库层,查到结果放入缓存后,再允许其他的业务进入,有效避免大量请求同时到达数据库层.



对于强一致性数据,则需要增加分布式锁来进行管控,与单个服务加锁逻辑不同,所有微服务同一时间只允许一个业务进入.分布式锁可以考虑使用Redisson进行加锁,后面也会给出实例.

SpringCache解决缓存穿透、缓存雪崩、缓存击穿

SpringCache实际就已经考虑到了我们大多数的环境和问题,所以针对上面的问题,也有对应的解决方案,如何应用?来看下面的示例/配置

  • SpringCache解决缓存穿透

还在为缓存头疼?来看看更优雅的缓存方案(一)中的配置文件中,有这样一段配置

spring:
  cache:
   redis:
     cache-null-values: true

从字面意思就可以看出,这里配置允许进行null缓存,当然也就能满足我们解决缓存穿透的需求.

  • SpringCache解决缓存雪崩

上面其实已经说明了,解决缓存雪崩其实就是尽量为我们的缓存设置过期时间即可,配置文件中就默认了缓存的过期时间为3600ms,当然也可以在业>务层面进行自定义时间的设置

spring:
  cache:
   redis:
     time-to-live: 3600000
  • SpringCache解决缓存击穿(使用绝大多数环境)

解决缓存击穿的思路就是在我们的服务中加锁,只需添加sync=true,在业务中就是@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync = true). 即可达到在函数执行前加锁,执行后解锁的功能. SpringCache虽然已经为我们实现了这个功能,但它并不是分布式锁,当我们部署了多个微服务的时候,只能保证单个微服务每次只通过一个业务请求,多个微服务还是存在多个请求同时到达数据库层的可能.不过,即使我们有几十几百个相同的微服务,同一时间也最多只会有几十上百个请求到达数据库层,对于数据库层的压力也并不大,所以说SpringCache在缓存击穿方面已经适用于绝大多数的环境了.

示例演示

对于缓存穿透、缓存雪崩仅需要配置文件即可,那么我们来演示一下SpringCache的防止缓存击穿效果
首先我们把CacheServiceImpl中的getAllProducts上的sync=true去掉

    @Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
    @Override
    public List<TestProduct> getAllProducts() {
        System.out.println("查询缓存信息" + new Date().toString());
        return getProductsFromDB(3);
    }

来想一下,此时访问localhost:8000/test/getCache的时候,会直接到CacheServiceImpl中的getAllProducts的方法中,然后会从数据库中获取数据,耗时10s,在10s内,如果有新的请求到达服务器,那么由于没有添加任何锁,所以第二个请求也会去数据库中查询数据,耗时10s,也就是我们所说的缓存穿透
首先访问localhost:8000/test/getCache,然后在10s内再次访问localhost:8000/test/getCache
来看一下控制台的日志

image.png
10:52:01客户端发起了一次请求,在10:52:02又发起了一次新的请求,这两个请求查询相同的内容,但是都触发了查询数据库的逻辑,可见此处已经造成了缓存穿透.

这次,我们把CacheServiceImpl中的getAllProducts上的sync=true加上

    @Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync=true)
    @Override
    public List<TestProduct> getAllProducts() {
        System.out.println("查询缓存信息" + new Date().toString());
        return getProductsFromDB(3);
    }

再来访问localhost:8000/test/getCache,然后在10s内再次访问localhost:8000/test/getCache
来看一下控制台的日志

image.png
我们仍然是在10s内发送了两个请求,但是查询数据库的逻辑仅仅调用了一次,可见,此处添加了sync=true之后,SpringCache已经为我们的函数进行了加锁,在单个服务中防止了缓存击穿的发生.

当然,像前面所说,SpringCache解决的是单机的缓存击穿,面对多个相同的微服务,它仍然存在一些问题,我们创建一个一模一样的服务redisTest2端口设置为8001
现在访问localhost:8000/test/getCache,然后在10s内再次访问localhost:8001/test/getCache
redisTest1控制台输出如下

image.png
redisTest2控制台输出如下
image.png
可见我们的两次请求,在多个微服务的情况下,仍然无法完全避免缓存击穿,无法满足我们对于数据强一致性的要求.要解决这个问题,我们就要考虑使用分布式锁了,下一节,我们会使用分布式锁解决这个问题.

以上内容转载请注明出处,同时也请大家不吝你的关注和下面的赞赏
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

相关文章

网友评论

      本文标题:还在为缓存头疼?来看看更优雅的缓存方案(二)

      本文链接:https://www.haomeiwen.com/subject/lopmeltx.html