Spring Cache 带你飞(二)

接着上一篇讲了 Spring Cache 如何被 Spring Aop 代理加载对应的代码,以及何如注入相关界面逻辑。

Spring Cache 带你飞(一)

本篇我们围绕两个要点展开:

  • 一个数据是如何被Spring Cache 放入缓存的。

  • Spring Cache 如何扩展存储源,即支持不同的缓存技术。

Spring Cache 的数据存储之路#

Spring Cache 相关的注解有 5 个:

  • @Cacheable 在调用方法的同时能够根据方法的请求参数对结果进行缓存。
  • @CachePut 调用发放的同时进行 Cache 存储,作用于方法上。
  • @CacheEvict 删除,作用于方法上。
  • @Caching 用于处理复杂的缓存情况,一次性设置多个缓存,作用于方法上。
  • @CacheConfig 可以在类级别上标注一些公用的缓存属性,所有方法共享。
@Cacheable

@Cacheable 是我们最常使用的注解:

Copy
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; String key() default ""; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; String condition() default ""; String unless() default ""; boolean sync() default false; }

cacheNamesvalue 这两个属性任意使用一个都可以,它们的作用可以理解为 key 的前缀。

Copy
@Cacheable(value = "user:cache") public User findById(String id) { User user = this.getById(id); if (user != null){ System.out.println("user.name = " + user.getName()); } return user; }

key 和 keyGenerator 是互斥的一对。当指定了 key 的时候就会使用你指定的 key + 参数 作为缓存 key。否则则使用默认 keyGenerator(SimpleKeyGenerator)或者你自定义的 Generator 来生成 key。

默认的 SimpleKeyGenerator 通过源码我们能看到它的生成规则:

Copy
public static Object generateKey(Object... params) { if (params.length == 0) { return SimpleKey.EMPTY; } if (params.length == 1) { Object param = params[0]; if (param != null && !param.getClass().isArray()) { return param; } } return new SimpleKey(params); }
  • 如果方法没有入参则抛异常,即必须要有入参才能构建 key;
  • 如果只有一个入参,则使用该入参作为 key=入参值。
  • 如果有多个入参则返回包含所有入参的构造函数 new SimpleKey(params)

Spring 官方推荐使用显式指定 key 的方式来生成 key。当然你也可以通过自定义 KeyGenerator 来实现自己制定规则的 key 生成方式,只需要实现 KeyGenerator 接口即可。

注意 key 属性为 spEL 表达式,如果要写字符串需要将该字符串用单引号括起来。比如我们有如下配置:

Copy
@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name") public String getName(String name) { return "hello:" + name; }

假设 name = xiaoming,那么缓存的 key = userInfo::p_xiaoming

condition 参数的作用是限定存储条件:

Copy
@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name",condition = "#sex == 1") public String getName(String name, int sex) { return "hello:" + name; }

上例限制条件为 sex == 1 的时候才写入缓存,否则不走缓存。

unless 参数跟 condition 参数相反,作用是当不满足某个条件的时候才写入缓存。

sync 字段上一篇说过,多线程情况下并发更新的情况是否只需要一个线程更新即可。

还有个属性 cacheManager 比较大头放在后面单独说,从命名上能看出它是 cache 的管理者,即指定当前 Cache 使用何种 Cache 配置,比如是 Redis 还是 local Cache 等等。这也是我们这一篇要讨论的重点。

@CacheConfig

CacheConfig 注解包含以下配置:

Copy
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CacheConfig { String[] cacheNames() default {}; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; }

如果你在一个类中使用多个 Cache 注解,并且这些 Cache 注解有公共的基础操作,比如:使用相同的 Cache key 生成规则,使用相同的 Cache Name 前缀等等,那么你就可以定义一个 CacheConfig 来统一单独管理这些 Cache 操作。

Copy
@CacheConfig(cacheNames = "user") public class UserService { @Cacheable(key = "#userInfoDTO.uid") public GirgirUser.UserInfo getUser(UserInfoDTO userInfoDTO) { return xxx; } @Cacheable(key = "'base_' + #userInfoDTO.uid") public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) { return xxx; } }

上面示例中的 两个 Cache Key 都会有一个公共前缀 ”user“。需要注意的是:CacheConfig 注解的优先级高于同类当中别的注解,如果你在 CacheConfig 中配置了 cacheNames,方法中也配置了,那么 CacheConfig 中的 cacheNames 会覆盖掉方法上的配置。

@Caching

@Caching 注解适用于复杂缓存操作的场景,当你有多个缓存操作的需求,比如下例:你需要先删除就缓存,再插入新数据到缓存:

Copy
@Caching(evict = @CacheEvict(key = "'base' + #userInfoDTO.uid"), put = @CachePut(key = "'base' + #userInfoDTO.uid")) public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) { return xxx; }

那么你可以使用 @Caching 注解来操作多个缓存。

注解的使用就说到这里,其余几个注解的配置基本同 @Cacheable 差不多,剩下的大家可以自己学习。接下来我们要说的重点来了:待缓存的数据到底是如何被存储起来的。Spring Cache 如何知道当前要使用的数据源。

Spring EL 对 Cache 的支持

Name Location Description Example
methodName Root object 被调用的方法的名称 #root.methodName
method Root object 被调用的方法 #root.method.name
target Root object 当前调用方法的对象 #root.target
targetClass Root object 当前调用方法的类 #root.targetClass
args Root object 当前方法的参数 #root.args[0]
caches Root object 当前方法的缓存集合 #root.caches[0].name
Argument name Evaluation context 当前方法的参数名称 #iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias).
result Evaluation context 方法返回的结果(要缓存的值)。只有在 unless 、@CachePut(用于计算键)或@CacheEvict(beforeInvocation=false)中才可用.对于支持的包装器(例如Optional),#result引用的是实际对象,而不是包装器 #result

Spring Cache 数据源配置#

Spring 在 application.yml 中提供配置文件支持,通过配置 spring.cache.type 标签来指定当前要使用的存储方案,目前支持的有:

Copy
public enum CacheType { GENERIC, JCACHE, EHCACHE, HAZELCAST, INFINISPAN, COUCHBASE, REDIS, CAFFEINE, SIMPLE, NONE; private CacheType() { } }

使用的时候需要引入相关存储对应的 jar 包以及相关的配置。

Java Caching 定义了 5 个核心接口,分别是 CachingProvider, CacheManager, Cache, Entry和 Expiry

  • CachingProvider 用于配置和管理 CacheManager,目前它只有一个唯一的实现类 EhcacheCachingProvider,ehcache 也是 Spring 默认提供的实现之一。其余的第三方缓存组件都没有用到。
  • CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache,这些 Cache 存在于 CacheManager 的上下文中。一个 CacheManager 仅被一个 CachingProvider 所拥有。
  • Cache 是一个类似 Map 的数据结构并临时存储以 Key 为索引的值。一个 Cache 仅被一个 CacheManager 所拥有。
  • Entry是一个存储在 Cache 中的 key-value 对。
  • Expiry 每一个存储在 Cache 中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过 ExpiryPolicy 设置。

Spring 定义了org.springframework.cache.CacheManagerorg.springframework.cache.Cache接口来统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术抽象接口,Cache 接口包含了缓存的各种操作。

针对不同的缓存方案需要提供不同的 CacheManager,Spring提供的实现类包括:

  • SimpleCacheManager:使用检点的 Collection 来存储缓存,主要用来测试
  • ConcurrentMapCacheManager:使用 ConcurrentMap 来存储缓存
  • NoOpCacheManager:仅测试用途,不会实际存储缓存
  • EhCacheManager:使用 EhCache 作为缓存技术
  • GuavaCacheManager:使用 Google Guava 的 GuavaCache 作为缓存技术
  • HazelcastCacheManager:使用 Hazelcast 作为缓存技术
  • JCacheManager:支持 JCache(JSR—107)标准的实现作为缓存技术
  • RedisCacheManager:使用 Redis 作为缓存技术

CacheManager 的加载来自于 spring.factories 文件中的配置:org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,即在 Spring启动的时候加载:

Copy
@Configuration @ConditionalOnClass(CacheManager.class) @ConditionalOnBean(CacheAspectSupport.class) @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver") @EnableConfigurationProperties(CacheProperties.class) @AutoConfigureBefore(HibernateJpaAutoConfiguration.class) @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, RedisAutoConfiguration.class }) @Import(CacheConfigurationImportSelector.class) public class CacheAutoConfiguration { ...... }

那不同的存储实现是如何加载各自的 CacheManger 的呢?我们就拿 Redis 来说,在配置类:

Copy
@Configuration @AutoConfigureAfter({RedisAutoConfiguration.class}) @ConditionalOnBean({RedisConnectionFactory.class}) @ConditionalOnMissingBean({CacheManager.class}) @Conditional({CacheCondition.class}) class RedisCacheConfiguration { ...... @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader())); List<String> cacheNames = this.cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet(cacheNames)); } return (RedisCacheManager)this.customizerInvoker.customize(builder.build()); } ...... }

Redis 的配置类启动的时候先检查 CacheManager 是否有加载成功,有的话则去执行各种配置相关操作。上面代码截出来了初始化 RedisCacheManager 的步骤。RedisCacheManager 实现了 CacheManager 接口。

当使用 RedisCacheManager 进行存储的时候,通过被包装的 Cache 对象来使用相关的存储操作,我们看一下 RedisCache 对应的操作:

Copy
public class RedisCache extends AbstractValueAdaptingCache { ...... public synchronized <T> T get(Object key, Callable<T> valueLoader) { ValueWrapper result = this.get(key); if (result != null) { return result.get(); } else { T value = valueFromLoader(key, valueLoader); this.put(key, value); return value; } } public void put(Object key, @Nullable Object value) { Object cacheValue = this.preProcessCacheValue(value); if (!this.isAllowNullValues() && cacheValue == null) { throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", this.name)); } else { this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl()); } } ...... }

可以看到 Redis 的存储使用的是普通的 KV 结构,value 的序列化方式是 yml 文件中的配置。另外很重要的一点是 ttl 的配置,这里能看到也是获取配置文件的属性。所以当你想给每个 key 单独设置过期时间的话就不能使用默认的 Redis 配置。而是需要自己实现 CacheManager。

posted @   rickiyang  阅读(1763)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
CONTENTS

"万一有人喜欢我呢"