缓存是实际工作中常常使用的一种提高性能的方法, 我们会在很多场景下来使用缓存。比如在访问不常更新的数据或查询耗时的数据表的时候,把数据存到 Redis 中,下次访问的时候直接从 Redis 中读取。

本文主要简介 SpringBoot 使用 @Cacheable 注解,并缓存数据到 Redis 中。如果对 Redis 不熟悉的请查看我以前的文章 SpringBoot API 系列(六) Redis

在 SpringBoot2 中,需要对 RedisCacheManager 进行配置。在 RedisConfig.java 中添加以下代码:

@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate) {
  RedisCacheWriter redisCacheWriter = RedisCacheWriter
      .nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));

  RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
      .defaultCacheConfig()
      .entryTtl(Duration.ofMinutes(30L)) // 缓存时间 30 分钟
      .disableCachingNullValues() // 空值不缓存
      .serializeValuesWith( // Value 序列化
          RedisSerializationContext.SerializationPair.fromSerializer(
              redisTemplate.getValueSerializer()
          )
      );

  return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}

当然不要忘了在项目入口添加 @EnableCaching 。

将 @Cacheable 注解添加到需要缓存的函数上:

@Override
@Cacheable(value = "getHomeInfo")
public HomeInfo getHomeInfo() {
    HomeInfo homeInfo = viewMapper.selectHomeInfo();
    return homeInfo;
}


可以看到 HomeInfo 的数据已经保存到了 Redis 中,再次执行 getHomeInfo 函数,控制台并没有再次打印 SQL 语句,但返回了数据。说明第二次返回的数据是从 Redis 中读取的缓存数据。


@Cacheable 可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。

例如,当缓存的函数中包含参数的时候, 标记 Key 就会在 Redis 中以此为 Key 存储数据,不同数据得以区分。


此外,还有更多的注解:

@CachePut

在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

//@CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。
@CachePut("users")//每次都会执行方法,并将结果存入指定的缓存中
public User find(Integer id) {
  returnnull;
}

@CacheEvict 是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

allEntries属性:allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。 

beforeInvocation属性:清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。 

@Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。 

@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
    @CacheEvict(value = "cache3", allEntries = true) })
public User find(Integer id) {
  returnnull;
}

PS:如果存在 LocalDateTime 等时间格式的数据,应将其序列化后传输,否则读取时可能会有异常。解决方案,在 RedisConfig.java 中添加以下代码:

/**
 * 默认日期时间格式
 */
private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

  // LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
  JavaTimeModule javaTimeModule = new JavaTimeModule();
  javaTimeModule.addSerializer(LocalDateTime.class,
    new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  javaTimeModule.addSerializer(LocalDate.class,
    new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  javaTimeModule.addSerializer(LocalTime.class,
    new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  javaTimeModule.addDeserializer(LocalDateTime.class,
    new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  javaTimeModule.addDeserializer(LocalDate.class,
    new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  javaTimeModule.addDeserializer(LocalTime.class,
    new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  objectMapper.registerModule(javaTimeModule);

  serializer.setObjectMapper(objectMapper);

  RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  redisTemplate.setConnectionFactory(redisConnectionFactory);
  redisTemplate.setKeySerializer(new StringRedisSerializer());
  redisTemplate.setValueSerializer(serializer);
  redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  redisTemplate.setHashValueSerializer(serializer);
  redisTemplate.afterPropertiesSet();

  return redisTemplate;
}