Spring 使用 Cache 解析及使用不同类型的 Cache

要在一个 Spring 应用中开启缓存方法返回结果的功能很简单,不需要额外的依赖,相关的的注解  @Cacheable, @CacheConfig, @CachePut, @CacheEvict, @EnableCache 等来自 spring-context 包。默认的的 Cache 实现是把数据存入到 ConcurrentMap 中,所以数据一直在内存中,除非显式的调用被 @CacheEvict 的方法来清理。实际进行数据缓存时会有更复杂的策略,如元素个数,占用内存,过期时间,何时使用磁盘等,而且不同的数据类型应有不同的缓存策略。

因此,除了使用默认的 ConcurrentMap 作为缓存外,还可通过配置属性 spring.cache.type 来使用其他类型的缓存,如 Caffeine, Couchbase, EhCache, INfinispan, JCache, Redis 等,或自定义 CacheManager 来使用 Guava Cache。

先来看来简单使用 Spring Cache 的步骤

假设我们调用 UserRepository.getUser(id) 方法, id 相同的话首先取缓存中的数据,UserRepository 的代码如下
1@Repository
2public class UserRepository {
3
4    @Cacheable("users")
5    public User getUser(String id) {
6        System.out.println("load user by id from database");
7        return new User(id, "Yanbin", "anywhere");
8    }
9}

我们首先心里要清楚, @Cacheable 是通过代理的方式生效的,代理在调用 getUser(id) 之前检查缓存中是否有相应的数据,有则不调用 getUser(id) 方法,否则调用,并把结果存入缓存以备用。因为要实现代理,所以 @Cacheable 注解方法所在的实例必须是一个 Spring Bean, 而且要从外部调用 getUser(id) 方法。

现在来使用缓存,代码写在  App 类中
 1@SpringBootApplication
 2@EnableCaching
 3public class App implements ApplicationRunner {
 4
 5    @Autowired
 6    private UserRepository userRepository;
 7
 8    public static void main(String[] args) {
 9        SpringApplication.run(App.class, args);
10    }
11
12    @Override
13    public void run(ApplicationArguments args) throws Exception {
14        IntStream.range(0, 3).forEach(i ->
15                System.out.println(userRepository.getUser("001"))
16        );
17        System.out.println(userRepository.getUser("002"));
18        System.out.println(userRepository.getUser("001"));
19    }
20}

运行结果如下:
load user by id from database
yanbin.blog.User@54ec8cc9
yanbin.blog.User@54ec8cc9
yanbin.blog.User@54ec8cc9
load user by id from database
yanbin.blog.User@1edb61b1
yanbin.blog.User@54ec8cc9
相同的 id 后缓都是从缓存中获得数据而不用实际调用 getUser(id) 方法。

再次强调 Spring Cache 生效的几个必要条件
  1. @EnableCaching 启用缓存,否则 @Cacheable  等注解不产生任何作用
  2. 被 @Cacheable 注解方法所在实例必须是一个 Spring Bean, 直接用  new UserRepository().getUser("001") 不会使用缓存
  3. 被 @Cacheable 注解的方法必须从外部调用,否则不会经过代理。UserRepository.foo() 方法调用 getUser("001") 也不会使用缓存

如果打个断点,就能看到在正常的调用栈之间添加了很多步骤

解析 Spring Cache 的实现

在我们使用 @EnableCaching 后,将会引入 SimpleCacheConfiguration, 在其中会调用 new ConcurrentMapCacheManager() 创建一个 CacheManager, 该 CacheManager 在用 @Cacheable(cacheNames="users") 检测到没有相应的 users 缓存实例 则会创建它,等于缓存是动态创建的。缓存是通过 ConcurrentMapCacheManager.createConcurrentMapCache(name) 创建的,是一个 ConcurrentHashMap 实例。

缓存数据的分类就是通过 @Cacheable 的 cacheNames 参数达成的,比如
@Cacheable("users")
public User getUser(String id); @Cacheable("roles")
public User getRole(String roleId);
@Cacheable 的 cacheNames 还能指定数据同时存储到多个 cache 中去。

Spring Cache 其他相关的概念可在 @CacheConfig 中看到
  1. keyGenerator:  cache key  的生成器,默认为 SimpleKeyGenerator,把方法的所有参数进行 hash 得到一个 key
  2. cacheManager: 可以定义自己的 cacheManager, 默认为 ConcurrentMapCacheManager
  3. cacheResolver: @Cacheable 可根据条件来选择对应的 cache,默认为 SimpleCacheResolver

当 Spring 在调用 @Cacheable("users") 之前,会从配置的 cacheManager 由 users 获得相应的 cache, 如 ConcurrentMapCacheManager 的方法
 1public Cache getCache(String name) {
 2    Cache cache = this.cacheMap.get(name);
 3    if (cache == null && this.dynamic) {
 4        synchronized (this.cacheMap) {
 5            cache = this.cacheMap.get(name);
 6            if (cache == null) {
 7                cache = createConcurrentMapCache(name);
 8                this.cacheMap.put(name, cache);
 9            }
10        }
11    }
12    return cache;
13}

users 对应的 Cache 就直接用,没有就调用 createConcurrentMapCache() 动态创建一个

从以上我们大概知道 Spring Cache 有哪些扩展点,keyGenerator, cacheManager, cacheResolver, 还有 CacheManagerCustomizer。@Cacheable和 @CachePut 的 key 属性还能使用 SpEL 表达式来选择 key, 如
1@CachePut(value="users", key="#result.id")
2User getUser(String id, String condition)

支持的属性有 #root.args, #root.caches, #root.target, #root.targetClass, #root.method, #root.methodName, #result, #Arguments

另外,我们主要聚焦 @Cacheable 注解,它还有 condition, unless 和 sync 来控制缓存的条件和是否同步存取缓存数据(默认为 false)

自定义不同的缓存实现

系统中不同类型的数据我们需要配置不一样的缓存策略,这就要对缓存进行定制,有多种式
  1. 定义不同的 cacheManager, 在 @Cacheable 中通过 cacheManager 选择。多个 cacheManager 时须标明一个是 @Primary
  2. 自定义 cacheResolver, 在 @Cacheable 中指定 cacheResolver 属性,根据条件选择哪个 CacheManager
  3. 自定义 cacheManager, 在 cacheManager 中根据 cacheName 来选择不同的 Cache. 本人倾向于这种方式

后面的示例用到了 Guava Cache 实现,所以需先引入它
1<dependency>
2    <groupId>com.google.guava</groupId>
3    <artifactId>guava</artifactId>
4    <version>31.1-jre</version>
5</dependency>

自定义多个 cacheManager

 1@Configuration
 2public class AppConfig {
 3
 4    @Bean
 5    @Primary
 6    public CacheManager rolesCacheManager() {
 7        return new ConcurrentMapCacheManager("roles"){
 8            public Cache createConcurrentMapCache(String name){
 9                return new ConcurrentMapCache(name, CacheBuilder.newBuilder()
10                        .expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(2).build().asMap(), false);
11            }
12        };
13    }
14
15    @Bean
16    public CacheManager usersCacheManager() {
17        return new ConcurrentMapCacheManager("users"){
18            public Cache createConcurrentMapCache(String name){
19                return new ConcurrentMapCache(name, CacheBuilder.newBuilder()
20                                .expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(1).build().asMap(), false);
21            }
22        };
23    }
24}

在 UserRepository.getUser(id) 的 @Cacheable 需选择 cacheManager="usersCacheManager"
1    @Cacheable(value = "users", cacheManager = "usersCacheManager")
2    public User getUser(String id) {

如果不指 cacheManager, 将会使用默认(@Primary) 的 rolesCacheManager, 它没有 users cache, 所以会报错
Cannot find cache named 'users' for Builder ....
如果不标明一个 cacheManager 是 @Primary, 启动时会报错
No qualifying bean of type 'org.springframework.cache.CacheManager' available: expected single matching bean but found 2: rolesCacheManager,usersCacheManager
usersCache 设置最多一个元素,所以执行相同 App 的输出为
load user by id from database
yanbin.blog.User@54ec8cc9
yanbin.blog.User@54ec8cc9
yanbin.blog.User@54ec8cc9
load user by id from database
yanbin.blog.User@503d56b5
load user by id from database
yanbin.blog.User@72bca894
存入 002 后, 011 被驱逐出去了,所以再次调用 getUser("001") 又重新创建了一个新的元素,而非来自于缓存中

自定义 CacheResolver

在上面定义了 usersCacheManager 和 rolesCacheManager 两个 Bean 的 AppConfig 中再加上一个 Bean
 1    @Bean
 2    public CacheResolver myCacheResolver(List<CacheManager> cacheManagers) {
 3        return context -> {
 4            Collection<Cache> caches = new ArrayList<>();
 5            cacheManagers.forEach(cacheManager -> {
 6                cacheManager.getCacheNames().forEach(cacheName -> {
 7                   if(context.getOperation().getCacheNames().contains(cacheName)) {
 8                       caches.add(cacheManager.getCache(cacheName));
 9                   }
10                });
11            });
12            return caches;
13        };
14    }

根据注解 @Cacheable 中的 cacheNames 中定位到需要的 Cache

然后在 @Cacheable 中指定 cacheResolver="myCacheResolver"
1    @Cacheable(value = "users", cacheResolver = "myCacheResolver")
2    public User getUser(String id) {

执行效果保持一至

自定义 cacheManager

这种方式我们只需要定义一个 cacheManager, 也不用 cacheResolver, 在 AppConfig 中的 Bean 定义为
 1@Configuration
 2public class AppConfig {
 3
 4    @Bean
 5    public CacheManager cacheManager() {
 6        Map<String, Cache> cacheMap = Stream.of(
 7                new ConcurrentMapCache("users", CacheBuilder.newBuilder()
 8                        .expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(1).build().asMap(), false),
 9                new ConcurrentMapCache("roles", CacheBuilder.newBuilder()
10                        .expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(10).build().asMap(), false))
11                .collect(Collectors.toMap(ConcurrentMapCache::getName, c->c));
12
13        return new CacheManager() {
14            @Override
15            public Cache getCache(@Nonnull String cacheName) {
16                return cacheMap.get(cacheName); // 根据 cacheName 选择 Cache
17            }
18
19            @Override
20            @Nonnull
21            public Collection<String> getCacheNames() {
22                return cacheMap.keySet();
23            }
24        };
25    }
26}

这个 cacheManager 将会替代原本默认的 ConcurrentMapCacheManager, 所以使用时不用指定 cacheManager,只要告诉 cacheName
1    @Cacheable(value = "users")
2    public User getUser(String id) {

系统中的 Cache 还是有所限制的好,不能无限的创建,容易导致问题,比如两个从不同数据源取 user 的 getUser(String id), 因为写错了 cacheName 就会造成性能的下降

DbUserRepository
1@Cacheable(value = "user")
2public User getUser(String id) {

S3UserRepository
1@Cacheable(value = "users")
2public User getUser(String id) {

这将会导致调用某一个 getUser(id) 方法时无法共享另一个 getUser(id) 的结果,因为 userusers 会分别创建两个 Cache。如果 CacheManager 中限定了不只有 users 时,则 Spring 在处理 @Cacheable(value="user") 会报告找不到 user Cache 的错误。

其他高级话题

Spring Cache 默认时使用 ConcurrentHashMap 来缓存数据,不能设置额外的缓存策略。通过自定义 CacheManager 可以设定自己的缓存策略,或者使用第三方的 Spring 已提供的实现配置,如 Caffeine, Redis 等,当然需要引入相应的缓存实现依赖。

通过声明一个继承自 CachingConfigurerSupport 的 Spring Bean, 可为 Spring Cache 提供默认的 CacheManager, CacheResolver, KeyGenerator, 和 CacheErrorHandler.

链接:
  1. Using Ehcache 3 in Spring Boot
永久链接 https://yanbin.blog/spring-cache-different-cache-types/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。