Spring与Cache

1. 前言

在做项目中往往遇到这样一些场景:从外部系统获取一些配置信息,它们被频繁读取但是却不经常修改,甚至是只读的数据。它们不要求非常强的实时性却有着非常高的访问频率。

如果每次都重新发起一次 HTTP 请求无疑是对系统资源的一种浪费。常见的做法是将这些外部数据以缓存的形式存储下来。

下面介绍一下 Spring 自带的 Cache 功能。

2. 几种基本注解

  • @Cacheable:开启缓存,指定命名空间参数(可以指定多个)。

  • @CacheEvict:指定缓存的清除,如果使用 allEntries = true 则会删除所有缓存。

  • @CachePut:首先运行目标方法然后将返回结果加入缓存。

  • @CacheConfig:在类的级别指定缓存的配置信息,比如缓存的名字或者 CacheManager

  • @Caching:由于一个方法上可以指定多个缓存,而 Java 不允许同一个注解在同一个地方出现多次,因此 @Caching 注解来组织。

3. 常见的缓存存储

Spring 虽然提供了缓存的框架,但是缓存的具体实现还分为很多种,常见的有 ConcurrentMapCacheGuavaCache 以及基于 Redis 的实现。

3.1 ConcurrentMapCache

ConcurrentMapCache 是一种简单的缓存实现,不能进行配置。那么 ConcurrentMapCache 缓存的生命周期是永远,除非使用 @CacheEvict 或者 @CachePut 否则缓存不会失效或者更新。

3.2 GuavaCache

这是基于 Guava 缓存的一种实现,我们可以像使用 Guava 中的 Cache 一样进行配置,比如缓存的失效时间、缓存的最大容量等等。

3.3 基于 Redis 的缓存

上文无论 ConcurrentMapCache 也好、GuavaCache 也好,都是基于本机的内存,无法在多个节点共享。基于 Redis 的缓存才能实现分布式缓存。

4. 代码示例

首先给出代码的依赖:

  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>com.avalon-inc</groupId>
          <artifactId>web-api-common</artifactId>
          <version>1.0-SNAPSHOT</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-cache</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
  </dependencies>

然后是 Redis 的配置,见 application.properties

  spring.redis.host=localhost
  spring.redis.port=6379

接着进入代码配置的正文,使用 Cache 的第一件事是开启对缓存的支持:

  package com.avaloninc.springcacheexample ;
  
  import org.springframework.boot.SpringApplication;
  import org.springframework.boot.autoconfigure.SpringBootApplication;
  import org.springframework.cache.annotation.EnableCaching;
  
  /**
   * @Author: wuzhiyu.
   * @Date: 2018-05-18 02:46:01.
   * @Description:
   */
  @SpringBootApplication
  @EnableCaching
  public class Main {
    public static void main(String[] args) {
      SpringApplication.run(Main.class);
    }
  }

然后我们执行最小化的设置,注册三个 CacheManager

  package com.avaloninc.springcacheexample;
  
  import com.google.common.cache.CacheBuilder;
  
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.cache.CacheManager;
  import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
  import org.springframework.cache.guava.GuavaCacheManager;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.context.annotation.Primary;
  import org.springframework.data.redis.cache.RedisCacheManager;
  import org.springframework.data.redis.connection.RedisConnectionFactory;
  import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  import org.springframework.data.redis.core.RedisTemplate;
  import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
  import org.springframework.data.redis.serializer.StringRedisSerializer;
  
  import java.util.concurrent.TimeUnit;
  
  /**
   * @Author: wuzhiyu.
   * @Date: 2018-06-12 01:44:20.
   * @Description:
   */
  @Configuration
  public class Config {
    @Bean("concurrentMapCacheManager")
    @Primary
    public CacheManager getDefaultCacheManager() {
      return new ConcurrentMapCacheManager();
    }
  
    @Bean("guavaCacheManager")
    public CacheManager getGuavaCacheManager() {
      GuavaCacheManager guavaCacheManager = new GuavaCacheManager();
      guavaCacheManager.setCacheBuilder(
          CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(5, TimeUnit.SECONDS));
      return guavaCacheManager;
    }
  
    @Bean
    public JedisConnectionFactory getRedisConnectionFactory(@Value("${spring.redis.host}") String host,
                                                            @Value("${spring.redis.port}") int port) {
      JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
      jedisConnectionFactory.setHostName(host);
      jedisConnectionFactory.setPort(port);
      return jedisConnectionFactory;
    }
  
    @Bean
    public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
      RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
      redisTemplate.setConnectionFactory(redisConnectionFactory);
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      return redisTemplate;
    }
  
    @Bean("redisCacheManager")
    public CacheManager getRedisCacheManager(RedisTemplate<String, Object> redisTemplate) {
      RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
      redisCacheManager.setDefaultExpiration(3);
      return redisCacheManager;
    }
  }

注意上面的 new ConcurrentMapCacheManager(),实例的构造函数有两种签名版本:

  • public ConcurrentMapCacheManager() {}

  • public ConcurrentMapCacheManager(String... cacheNames) { setCacheNames(Arrays.asList(cacheNames));}

如果使用无参构造,那么允许动态构造缓存,缓存数量可变。如果使用有参版本那么只能以固定个数的命名空间来创建固定个数的缓存。GuavaCacheRedisCache 同上。

接着我们编写使用缓存的具体代码:

  package com.avaloninc.springcacheexample ;
  
  import com.avaloninc.webapi.common.response.Response;
  import com.avaloninc.webapi.common.response.Responses;
  import lombok.extern.slf4j.Slf4j;
  import org.joda.time.DateTime;
  import org.springframework.boot.SpringApplication;
  import org.springframework.boot.autoconfigure.SpringBootApplication;
  import org.springframework.cache.annotation.CacheConfig;
  import org.springframework.cache.annotation.CacheEvict;
  import org.springframework.cache.annotation.CachePut;
  import org.springframework.cache.annotation.Cacheable;
  import org.springframework.cache.annotation.Caching;
  import org.springframework.cache.annotation.EnableCaching;
  import org.springframework.web.bind.annotation.DeleteMapping;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.PathVariable;
  import org.springframework.web.bind.annotation.PutMapping;
  import org.springframework.web.bind.annotation.RestController;
  
  /**
   * @Author: wuzhiyu.
   * @Date: 2018-05-18 02:46:01.
   * @Description:
   */
  @SpringBootApplication
  @EnableCaching
  @RestController
  @Slf4j
  @CacheConfig(cacheNames = "caches")
  public class Main {
    public static void main(String[] args) {
      SpringApplication.run(Main.class);
    }
  
    @GetMapping("/obj/{key}")
    @Caching(cacheable = {
        @Cacheable(cacheManager = "concurrentMapCacheManager"),
        @Cacheable(cacheManager = "guavaCacheManager"),
        @Cacheable(cacheManager = "redisCacheManager")
    })
    public Response<String> get(@PathVariable("key") String key) {
      String value = key.concat(" ").concat(DateTime.now().toString());
      log.info("Get the real value of " + key);
      return Responses.sucessResponse(value);
    }
  
    @DeleteMapping("/obj/{key}")
    @Caching(evict = {
        @CacheEvict(cacheManager = "concurrentMapCacheManager"),
        @CacheEvict(cacheManager = "guavaCacheManager"),
        @CacheEvict(cacheManager = "redisCacheManager")
    })
    public Response<String> del(@PathVariable("key") String key) {
      log.info("Evict the key " + key);
      return Responses.sucessResponse("success");
    }
  
    @PutMapping("/obj/{key}")
    @Caching(put = {
        @CachePut(cacheManager = "concurrentMapCacheManager"),
        @CachePut(cacheManager = "guavaCacheManager"),
        @CachePut(cacheManager = "redisCacheManager")
    })
    public Response<String> put(@PathVariable("key") String key) {
      String value = key.concat(" ").concat(DateTime.now().toString());
      log.info("Put the real value of " + key);
      return Responses.sucessResponse(value);
    }
  }

注意上文是为了演示 @Caching 才同时使用了三种缓存。实验显示多个缓存下 @Cacheable 只要有一个缓存没有失效那么就不会执行具体的方法体获取的新的值!如果其他缓存和 ConcurrentMapCache 一起使用而不调用 @CacheEvict 或者 @CachePut,那么其他缓存可能永远不会更新!

5. 总结

本文只是简单介绍了一下 SpringBoot 集成缓存以及几种常见的存储方式,具体的选择按需求场景确定即可。但是正确的缓存使用姿势或者更新策略可以结合现有的设计模式,灵活运用这几种注解实现选定的模式。

6. 参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注