avatar

15.SpringBoot整合之缓存篇(1)

一、从Java缓存规范(JSR107)说起

  1. 在JSR107中,定义了5个核心的接口,分别是CachingProvider, CacheManager, Cache, Entry和Expiry。具体解释如下:
    • CachingProvider:CacheingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
    • CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache。这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CacheingProvider所拥有。
    • Cache:Cache是一个类似Map数据结构并临时存储以Key为索引的值,一个Cache仅被一个CacheManger所拥有。
    • Entry:存储在Cache中的一个key-value对
    • Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目不能访问、更新、删除。缓存有效期可以通过ExpiryPolicy设置。
  1. 5个核心接口关系图

二、Spring的缓存抽象

  1. 在Spring3.1之后,Spring定义了org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR107)注解简化我们的开发。
    • Cache:缓存接口,定义统一的缓存操作。方便对不同的缓存组件进行整合。只需要实现该接口即可。实现类有:RedisCacheConcurrentMapCache等。
    • CacheManager:缓存管理器。管理各种缓存(Cache)组件。一个缓存管理器可以管理多个Cache组件
  2. 关系图
  3. 几个重要概念以及缓存注解解释
    1. Cache:这个上面也说过了,是Spring定义的缓存接口,用于统一管理各种缓存组件,每个缓存组件都有一个唯一的名字。
    2. CacheManager:这个也在上面说了,是Spring定义的缓存管理器,管理Cache的。一个缓存管理器可以管理多个Cache。
    3. @Cacheable:配置在方法定义上,作用就是根据方法的请求参数对其结果进行缓存。可以看成类似<方法参数,结果>结构的Map
    4. @CacheEvict:清空缓存
    5. @CachePut:保证方法被调用,但是又希望结果被缓存。
    6. @EnableCaching:开启基于注解的缓存
    7. keyGenerator:缓存数据时Key生成的策略
    8. serialize:缓存数据时value序列化策略。
      具体操作将在代码中给出
  4. @Cacheable注解主要参数解释
    1. value/cacheNames:指定缓存的名称,在Spring配置文件中定义。必须指定至少一个。例如:@Cacheable(value="myCache")或value={"cache1","cache2"}
    2. key:缓存的key,可以为空,为空时,则按照方法的所有参数进行组合。若指定,则要按照SpELl表达式编写。
      • 常用的SpEl表达式
        • 获取当前被调用的方法名:#root.methodName
        • 当前被调用的方法:#root.method.name
        • 当前被调用的目标对象:#root.target
        • 当前被调用的目标对象类:#root.targetClass
        • 当前被调用的方法的参数列表:#root.args[0]/#参数名/#p0/#a0 (其中0代表参数的索引,从0开始)
        • 当前方法调用使用的缓存对象:#root.caches[0].name(因为可以指定多个缓存,所以是数组形式)
        • 当前方法执行后的返回值(仅对当前方法执行之后的判断有效):#result
    3. keyGenerator:key的生成器,可以自己指定key的生成器的组件id。keyGenerator和key二选一使用
    4. cacheManager:指定缓存管理器,或者使用cacheResolver指定获取解析器。
    5. condition:指定符合条件的情况下才缓存,只有返回true才会进行缓存。也是使用SpEL表达式
    6. unless:是否缓存,当unless指定的条件为true,方法的返回值就不会被缓存,可以取到结果进行判断
    7. sync:是否使用异步模式

三、Spring缓存抽象的使用

  1. 新建一个SpringBoot项目,勾选core里面的cache组件,然后再勾选web组件。为了简单起见,我们就不连接数据库了。就用HashMap造一些数据。
  2. 以下为Dao的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Repository
public class UserDao {
private static Map<Integer,User> map = new HashMap<>();
static{
map.put(1,new User(1,"张三",18));
map.put(2,new User(2,"李四",19));
map.put(3,new User(3,"王五",17));
map.put(4,new User(4,"赵六",14));
map.put(5,new User(5,"王小二",20));
map.put(6,new User(6,"李小四",29));
map.put(7,new User(7,"赵明",34));
map.put(8,new User(8,"王柳",16));
map.put(9,new User(9,"李斯",17));
map.put(10,new User(10,"孙福",10));
}

public User getUser(int id){
//模拟调用数据库。如果控制台打印出了下面的语句,则说明是调用的数据库查询而不是缓存
System.out.println("use db select, id is" +id);
return map.get(id);
}
}
  1. 以下为Service的代码,这里暂时演示@Cacheable的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class UserService {
@Autowired
private UserDao userDao;
/*
1.使用@Cacheable注解标识要对该方法的返回值进行缓存。注意,使用缓存注解需要在启动类上声明@EnableCaching
2.value:指定使用的cache的名称。也就是这个数据缓存到哪个cache中。该值必须指定至少一个。
3.key:指定缓存到cache中的key。使用SpEL表达式指定
*/
@Cacheable(value = "UserCache",key = "#id")
public User getUser(Integer id){
return userDao.getUser(id);
}
}
  1. 以下为Controller的代码
1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;

@RequestMapping("/getUser/{id}")
public User getUser(@PathVariable("id") Integer id){
return userService.getUser(id);
}
}
  1. 以下为启动类代码
1
2
3
4
5
6
7
8
9
@SpringBootApplication
//开启基于注解的缓存
@EnableCaching
public class SpringbootCacheApplication {

public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
  1. 结果
    • 第一次访问http://localhost:8080/user/getUser/1得到的结果如下:
      • 网页效果
      • 控制台效果
    • 第二次访问/刷新网页结果如下:
      • 网页效果
      • 控制台效果
  2. 结论:
    • 如果需要使用Spring的缓存抽象。则需要如下几步,就可以简单使用了
      • 在Maven中引入Cache组件
      • 在启动类上声明@EnableCaching注解,表示开启基于注解的缓存
      • 在需要缓存数据的地方使用对应的注解即可

四、Spring缓存抽象原理及使用@Cacheable缓存基本流程分析

  1. Spring缓存抽象原理分析
    • 对SpringBoot有一点了解的同学就应该都知道,SpringBoot中的每一个组件,都会有一个或多个对应的XXXAutoConfiguration.在项目启动时自动配置。Cache也不例外。这次我们就来看看CacheAutoConfiguration。声明代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Configuration
      @ConditionalOnClass(CacheManager.class) //只有引入了CacheManager才会生效
      @ConditionalOnBean(CacheAspectSupport.class)
      @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
      @EnableConfigurationProperties(CacheProperties.class)
      @AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
      @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
      RedisAutoConfiguration.class })
      //导入CacheConfigurationImportSelector类,并执行
      @Import(CacheConfigurationImportSelector.class)
      public class CacheAutoConfiguration {
      ....
      }
    • 从上面的源码看,他会导入一个CacheConfigurationImportSelector类,并且执行它,那么我们就看看在这里执行了什么吧。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      static class CacheConfigurationImportSelector implements ImportSelector {

      @Override
      public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      //CacheType是枚举类型。取值如下:
      //GENERIC、JCACHE(JSR107)、EHCACHE、HAZELCAST、INFINISPAN、COUCHBASE、REDIS、CAFFEINE、SIMPLE、NONE
      CacheType[] types = CacheType.values();
      String[] imports = new String[types.length];
      //循环获取CacheType里面的值
      for (int i = 0; i < types.length; i++) {
      //通过type获取Configuration的类名,赋给imports
      imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
      }
      return imports;
      }
      }
    • CacheConfigurationImportSelector中会调用CacheConfigurations.getConfigurationClass()方法获取Configuration的类名。来一起看下吧。
      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

      final class CacheConfigurations {

      private static final Map<CacheType, Class<?>> MAPPINGS;
      //这里面就是各种各样的Configuration类
      static {
      Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
      mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
      mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
      mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
      mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
      mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
      mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
      mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); //redis的Configuration
      mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
      mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
      mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
      MAPPINGS = Collections.unmodifiableMap(mappings);
      }

      private CacheConfigurations() {
      }

      //通过type来获取configuration类的类名
      public static String getConfigurationClass(CacheType cacheType) {
      //从上面map中获取Configuration的类
      Class<?> configurationClass = MAPPINGS.get(cacheType);
      Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
      //返回类的全类名
      return configurationClass.getName();
      }
      //根据全类名获取类型
      public static CacheType getType(String configurationClassName) {
      for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
      if (entry.getValue().getName().equals(configurationClassName)) {
      return entry.getKey();
      }
      }
      throw new IllegalStateException(
      "Unknown configuration class " + configurationClassName);
      }

      }
    • 既然他引入了这么多个Configuration。那么默认是用的哪个呢?这里默认的使用的是**SimpleCacheConfiguration**
    • 那我们就去SimpleCacheConfiguration分析一波吧
      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
      @Configuration
      @ConditionalOnMissingBean(CacheManager.class)
      @Conditional(CacheCondition.class)
      class SimpleCacheConfiguration {
      //缓存配置相关文件
      private final CacheProperties cacheProperties;
      //缓存管理器定制器
      private final CacheManagerCustomizers customizerInvoker;
      //在创建对象的时候注入配置文件和定制器
      SimpleCacheConfiguration(CacheProperties cacheProperties,
      CacheManagerCustomizers customizerInvoker) {
      this.cacheProperties = cacheProperties;
      this.customizerInvoker = customizerInvoker;
      }

      @Bean
      //往容器中加入一个ConcurrentMapCacheManager的实例
      //这里可以看出,SimpleCacheConfiguration使用的是ConcurrentMapCacheManager
      public ConcurrentMapCacheManager cacheManager() {
      ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
      //从配置文件中读取缓存组件的名字。也就是说我们可以在配置文件中配置缓存的名字
      List<String> cacheNames = this.cacheProperties.getCacheNames();
      if (!cacheNames.isEmpty()) {
      cacheManager.setCacheNames(cacheNames);
      }
      //指定定制器并返回已经执行完定制器的cacheManager
      return this.customizerInvoker.customize(cacheManager);
      }
      }
    • 到此,Cache的自动配置算是分析完了。总结下下
      1. 在项目启动的时候,执行CacheAutoConfiguration类,导入CacheConfigurationImportSelector选择器
      2. CacheConfigurationImportSelector中通过CacheType获取对应类型的CacheConfiguration类的全类名
      3. 默认使用的是SimpleCacheConfiguration
      4. SimpleCacheConfiguration中往容器加入了一个ConcurrentMapCacheManager。也就是说,我们默认使用的CacheManager是ConcurrentMapCacheManager。
  2. @Cacheable基本流程分析
    • 上面说到我们默认使用的是ConcurrentMapCacheManager.那么我们在使用@Cacheable的时候,他获取的缓存对象是什么呢?下面就是他获取cache的方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public Cache getCache(String name) {
      //首先根据缓存的名字,也就是@Cacheable的value值去缓存map中获取缓存
      Cache cache = this.cacheMap.get(name);
      //如果获取不到对应的cache,就创建个
      if (cache == null && this.dynamic) {
      synchronized (this.cacheMap) {
      cache = this.cacheMap.get(name);
      if (cache == null) {
      //创建个缓存,并加入到cacheMap中
      cache = createConcurrentMapCache(name);
      this.cacheMap.put(name, cache);
      }
      }
      }
      return cache;
      }
    • 如果获取不到,就调用createConcurrentMapCache(name)创建缓存。代码如下:

      1
      2
      3
      4
      5
      6
      7
      protected Cache createConcurrentMapCache(String name) {
      SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
      //可以看出来,他是创建个ConcurrentMapCache。也就是说默认使用的Cache是ConcurrentHashMap
      return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256),
      isAllowNullValues(), actualSerialization);

      }
    • Cache拿到了,默认为ConcurrentHashMap,那么他是怎么往缓存中存取数据呢?

      • 1
        2
        3
        4
        public void put(Object key, @Nullable Object value) {
        //使用toStoreValue()序列化值,放到ConcurrentHashMap中
        this.store.put(key, toStoreValue(value));
        }
      • 1
        2
        3
        4
        protected Object lookup(Object key) {
        //直接通过key从缓存中去
        return this.store.get(key);
        }
    • 上面只是存取调用的方法,那么它的流程是怎样呢?我们在上面两个地方都下断点。访问一次。

      • 断点首先断在lookup()这个方法上。即说明,在执行具体方法前会先执行lookup()查看缓存中是否有值,如果有就会返回。如果没有就会去执行方法。随后,我们在堆栈中发现是通过CacheAspectSupport.findCachedItem()方法调用过来的。发现key是在该方法中生成的。它的源码如下:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
        Object result = CacheOperationExpressionEvaluator.NO_RESULT;
        for (CacheOperationContext context : contexts) {
        if (isConditionPassing(context, result)) {
        //在这里生成key
        Object key = generateKey(context, result);
        //然后通过生成的key,去Cache中找值
        Cache.ValueWrapper cached = findInCaches(context, key);
        //如果找到了,就直接返回
        if (cached != null) {
        return cached;
        }
        //找不到就返回null
        else {
        if (logger.isTraceEnabled()) {
        logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
        }
        }
        }
        }
        return null;
        }
    • 于是乎,我们得去看看这个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
      private Object generateKey(CacheOperationContext context, @Nullable Object result) {
      //使用context.generateKey进行key的生成。
      Object key = context.generateKey(result);
      if (key == null) {
      throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
      "using named params on classes without debug info?) " + context.metadata.operation);
      }
      if (logger.isTraceEnabled()) {
      logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
      }
      return key;
      }
      //调用该方法生成key
      protected Object generateKey(@Nullable Object result) {
      //判断是否在@Cacheable注解里指定了key的值或生成策略
      if (StringUtils.hasText(this.metadata.operation.getKey())) {
      EvaluationContext evaluationContext = createEvaluationContext(result);
      //如果有,则使用evaluator.key()按照指定的key生成
      return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
      }
      //如果在@Cacheable中不指定key的值。则执行下面的方法,生成key。通过断点调试,发现这里的keyGenerator为SimpleKeyGenerator
      //参数分别为:标注注解方法的类、标注注解方法名、标注注解方法的参数
      return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
      }
    • 通过断点调试发现,他默认使用的keyGenerator是SimpleKeyGenerator。下面我们看看SimpleKeyGenerator是怎么生成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
      //调用该方法进行key的生成
      public Object generate(Object target, Method method, Object... params) {
      //把标注注解方法的参数传递过去
      return generateKey(params);
      }

      public static Object generateKey(Object... params) {
      //如果没有参数,则返回一个空的Simplekey对象
      if (params.length == 0) {
      return SimpleKey.EMPTY;
      }
      //如果参数只有1个
      if (params.length == 1) {
      //去参数值
      Object param = params[0];
      //如果参数值不为空,并且不是数组就把参数值作为key返回
      if (param != null && !param.getClass().isArray()) {
      return param;
      }
      }
      //如果方法参数不止一个就创建个SimpleKey对象
      return new SimpleKey(params);
      }

      public SimpleKey(Object... elements) {
      Assert.notNull(elements, "Elements must not be null");
      this.params = new Object[elements.length];//创建个同等大小的数组
      System.arraycopy(elements, 0, this.params, 0, elements.length);//拷贝值
      //对参数进行散列
      this.hashCode = Arrays.deepHashCode(this.params);
      }
    • key的生成策略已经分析完了,下面总结下:

      • 默认使用的生成策略为SimpleKeyGenerator(指在使用注解的时候未声明key的值或生成策略)
      • 如果在使用注解时声明了key的值或者生成策略。那么就使用声明的值或策略生成key
      • 默认情况下,使用方法的参数作为key,根据方法的参数不同,又分为这几种情况
        • 参数为空
          • 返回new SimpleKey()对象作为key
        • 参数只有一个
          • 返回该参数的值作为key
        • 参数有多个
          • 返回new SimpleKey(params)对象作为key
    • 总结下流程吧

      1. 在方法调用前,先去通过定义的缓存的name去CacheManager<ConcurrentCacheManager>中查询Cache组件,如果没有就创建。并放入CacheManager的cacheMap中。
      2. 拿到cache组件后,首先根据生成的key调用Cache组件的lookup()查看缓存中是否有对应的值,如果有就直接返回
      3. 如果查不到值,就调用方法。并再方法调用后,调用Cache组件的put()把key和value存放到Cache中

五、结语

首先,这篇分析篇幅算比较长。希望可以耐心的看完。下面把所有的流程的总结作为结尾吧~

  1. 在项目跑起来后,会自动执行CacheAutoConfigration自动配置类。
  2. 在该自动配置类中,会导入CacheConfigurationImportSelector,在该类中会根据Cache的类型获取对应的Configuration
  3. 在我们没有引入其他的Cache组件下,默认使用的是SimpleCacheConfiguration
  4. 而在SimpleCacheConfiguration中,会为我们创建一个ConcurrentCacheManager缓存管理器。
  5. 如果方法标注了@Cacheable,则在目标方法执行前,会先通过我们定义的缓存名称,去ConcurrentCacheManager缓存管理器中寻找Cache组件。如果没有找到就帮我们创建个,并且加入到一个cacheMap中。
  6. 到此,我们已经拿到了Cache组件了。然后就会通过key的生成策略,去生成一个key。然后通过该key调用Cache的lookup()方法获取对应的value。如果value存在,则直接返回,不执行目标方法。
  7. 如果没有该value,则会执行目标方法,在执行目标方法后,会调用Cache的put()方法,把key和value保存到Cache中。
  8. 再次调用时,如果有值,就不会执行方法了。达到缓存的目的。

谢谢大家观看~下面将继续带来Spring缓存抽象其他的注解使用及原理分析。敬请期待~~


评论