1 分钟快速上手 Spring Cache
发布于 2022年 02月 21日 18:51
快速开始(从0到1)
如果你现在有一个现成的工程,你想给你工程的某个接口增加缓存,再不可以分布式缓存的情况下,你可以通过以下两步完成 Spring Cache
接入:
1、引用依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、给你需要增加缓存的接口或者方法加上注解
@Service
@CacheConfig(cacheNames = "myCache")
public class MyCacheService {
@CachePut(key = "#key", unless="#result == null")
public String save(String key) {
// do something
}
@Cacheable(key = "#key")
public String find(String key) {
// do something
}
}
当你完成这两步时,支持 local
缓存的方案已经完成了。
基本使用(默认配置)
快速开始部分,我们仅引入了一个依赖,然后对需要缓存的接口加了注解,其他什么配置都没有,所以这种方式使用的都是 Spring Cache
的默认配置。Spring Cache
的默认配置类是 CacheProperties
,简单看下有哪些配置属性:
属性 | 子属性 | 描述 |
---|---|---|
type | 缓存类型,根据环境自动检测(auto-detected) | |
cacheNames | 如果底层缓存管理器支持的话,要创建的以逗号分隔的缓存名称列表。通常,这将禁用动态创建额外缓存的能力。 | |
caffeine | spec:是创建缓存规范,具体见 CaffeineSpec 类 | Caffeine 作为缓存 |
couchbase | expiration:描述过期时间,默认情况下,内部 entries 不会过期 | Couchbase 作为缓存 |
ehcache | config: 用于创建 ehcache 所提供的配置文件 | EhCache 作为缓存 |
infinispan | config:用于创建 Infinispan 所提供的配置文件 | Infinispan 作为缓存 |
jcache | config:用于初始化缓存管理器的配置文件的位置。配置文件依赖于底层缓存实现。 | Jcache 作为缓存 |
provider:CachingProvider 实现的完全限定名,用于检索符合JSR-107的缓存管理器。仅当类路径上有多个JSR-107实现可用时才需要。 | ||
redis | timeToLive:缓存过期时间 | Redis 作为缓存 |
cacheNullValues:是否允许缓存 null 值 | ||
keyPrefix:key 前缀 | ||
useKeyPrefix:写入时是否使用 前缀 | ||
enableStatistics:是否开启缓存指标统计能力 |
Spring Cache
没有使用上表中的缓存,上表中所提到的缓存类型是在指定 type
时,对应所需的配置,默认情况下,在没有明确指定 type
时,使用的是 SIMPLE
,CacheType
所有枚举类型如下:
public enum CacheType {
/**
* Generic caching using 'Cache' beans from the context.
*/
GENERIC,
/**
* JCache (JSR-107) backed caching.
*/
JCACHE,
/**
* EhCache backed caching.
*/
EHCACHE,
/**
* Hazelcast backed caching.
*/
HAZELCAST,
/**
* Infinispan backed caching.
*/
INFINISPAN,
/**
* Couchbase backed caching.
*/
COUCHBASE,
/**
* Redis backed caching.
*/
REDIS,
/**
* Caffeine backed caching.
*/
CAFFEINE,
/**
* Simple in-memory caching.
*/
SIMPLE,
/**
* No caching.
*/
NONE
}
SIMPLE
对应的缓存器是基于内存的,其底层存储基于 ConcurrentHashMap
。
使用 redis 作为缓存
上述快速开始部分实现缓存存储是基于内存的,对于单体应用解决小流量接口缓存问题不大,但是在分布式环境和大流量接口场景下,是不行的。下面来对快速开始部分进行改造,实现目前常用的基于 Spring Cache + Redis
的方案。
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
注意:网上一些时间较久的文章使用的是 spring-boot-starter-redis
,这个依赖在 Spring Boot 1.4
版本之后被弃用了,改为使用 spring-boot-starter-data-redis
了,官方有明确说明,详见:mvnrepository.com/artifact/or…
2、指定 cache type
为 redis
spring.cache.type=redis
完成 1-2 时,就完成了基于 redis
默认配置的集成,此时连接的 redis
地址是 localshot:6379
;当然也可以通过配置文件来定制 redis
的配置,
#redis配置
#Redis数据库索引(缓存将使用此索引编号的数据库)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=localhost
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接超时时间 毫秒(默认2000)
#请求redis服务的超时时间,这里注意设置成0时取默认时间2000
spring.redis.timeout=2000
#连接池最大连接数(使用负值表示没有限制)
#建议为业务期望QPS/一个连接的QPS,例如50000/1000=50
#一次命令时间(borrow|return resource+Jedis执行命令+网络延迟)的平均耗时约为1ms,一个连接的QPS大约是1000
spring.redis.pool.max-active=50
#连接池中的最大空闲连接
#建议和最大连接数一致,这样做的好处是连接数从不减少,从而避免了连接池伸缩产生的性能开销。
spring.redis.pool.max-idle=50
#连接池中的最小空闲连接
#建议为0,在无请求的状况下从不创建链接
spring.redis.pool.min-idle=0
#连接池最大阻塞等待时间 毫秒(-1表示没有限制)
#建议不要为-1,连接池占满后无法获取连接时将在该时间内阻塞等待,超时后将抛出异常。
spring.redis.pool.max-wait=2000
此外,还可以通过创建缓存配置文件类可以设置缓存各项参数,比如缓存key 的过期时间,使用 key 前缀等,如下:
定义缓存过期时间
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
// 过期时间
.entryTtl(Duration.ofMinutes(60)));
}
自定义 key 前缀
key 前缀默认是 cacheName,比如你的 key 是 test,你的 cacheName 是 myCache,则默认情况下存入的 key 为:"myCache::test", 如果需要调整,可以通过如下方式调整
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
// 增加前缀
.prefixCacheNameWith("my-prefix::")));
}
修改之后,key 为 "my-prefix::myCache::glmapper"
。
除了这些 redis 配置之外,通过 @CacheConfig 注解可以看到,还有 keyGenerator、cacheManager 和 cacheResolver,这些也可以通过自己实现来完成定制化。
自定义 KeyGenerator
顾名思义,keyGenerator 是用来生成 key 的,如上面例子中的
@Cacheable(key = "#key")
public String find(String key) {
// do something
}
这里的 key 是通过 Spel 表达式从参数中获取的,当 Spel 表达式不能满足我们需求时,则可以使用自定义缓存 key 来实现,只需指定 KeyGenerator 接口的实现类的 bean 名称即可,如下
@Component
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
String key = params[0] + "-glmapper";
return key;
}
}
此时存储的 key 为:"my-prefix::myCache::glmapper-glmapper"
。
需要注意的是,keyGenerator 和 key 不能同时存在,比如:
@Cacheable(key = "#key", keyGenerator = "myKeyGenerator")
public String find(String key) {
System.out.println("execute find...");
return this.mockDao.find(key);
}
如果同时存在,则会抛出如下异常:
Both 'key' and 'keyGenerator' attributes have been set. These attributes are mutually exclusive: either set the SpEL expression used tocompute the key at runtime or set the name of the KeyGenerator bean to use.
自定义 CachManager
自定义 CacheManager 就是实现 CacheManager 接口即可,一般情况下,如果我们需要自定义 RedisConnectionFactory 和 RedisCacheConfiguration 的话,会用到自定义 CacheManager
@Bean(name = "myCacheManager")
public CacheManager myCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration
.ofSeconds(600L))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfiguration)
.build();
}
使用时,可以指定具体的 cacheManager
@Cacheable(keyGenerator = "myKeyGenerator", cacheManager = "myCacheManager")
public String find(String key) {
System.out.println("execute find...");
return this.mockDao.find(key);
}
自定义CacheResolver
CacheResolver 是缓存解析器,默认的 Cache 解析实现类是org.springframework.cache.interceptor.SimpleCacheResolver
,自定义 Cache 解析器需要实现CacheResolver 接口,使用方式和前面自定义 KeyGenerator 类似,即在注解属性 cacheResolver 配置自定义Bean名称。
CacheResolver 解析器的目的是从 CacheOperationInvocationContext 中解析出 Cache,
@Component
public class MyCacheResolver implements CacheResolver {
private final CacheManager cacheManager;
public MyCacheResolver(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Cacheable annotation = context.getMethod().getAnnotation(Cacheable.class);
BasicOperation operation = context.getOperation();
if (operation instanceof CacheableOperation) {
// do something
}
Collection<Cache> ret = new ArrayList<>();
// 根据注解 或 方法得到的 cacheName 去 getCache,再返回不同的过期时间的 Cache
String[] cacheNames = annotation.cacheNames();
for (String cacheName : cacheNames) {
ret.add(cacheManager.getCache(cacheName));
}
return ret;
}
}
条件缓存 condition 和 unless
最后再来关注下常见的条件缓存问题;有时候,一些值不适合缓存,可以使用 @Cacheable 的 condition 属性判读那些数据不缓存,它接收的是一个 Spel 表达式,该表达式的值是 true 或 false;true,数据被缓存,false不被缓存。
@Cacheable(key = "#key", condition = "#key.startsWith('glmapper::')")
public String find(String key) {
System.out.println("execute find...");
return this.mockDao.find(key);
}
key 必须是 "glmapper::"
开头的才允许缓存。
@Cacheable#unless 一般是对结果条件判读是否进行缓存使用的,这个示例使用的是入参作为判断条件,各位可以自己写一个根据结果进行缓存的示例,切记满足条件是不缓存。Spel #result变量代表返回值。
@CachePut(unless="#result == null", keyGenerator = "myKeyGenerator")
public String save(String model) {
System.out.println("execute save...");
this.mockDao.save(model, model);
return model;
}
如果返回结果是 null,则不缓存。
beforeInvocation 可能导致潜在的缓存不一致问题
beforeInvocation 是 CacheEvict 注解的属性,默认值为false,表示在调用方法之后进行缓存清理;如果设置true,表示在调用方法之前进行缓存清理。一般情况下推荐使用默认配置即可,如果设置成 true,有两种可能导致一致性问题:
- 在清理之后,执行方法执行,并发设置缓存。
- 注解的方法本身内部如果调用了填充缓存的方法。
总结
整体来看,Spring Cache 的上手难度不算大,其提供的注解能够覆盖大多数使用 cache 的场景,对于业务逻辑基本无侵入性。同时,Spring 也秉持了其一贯的作风,就是提供灵活的扩展机制,使得你可以自由的定制自己的各种功能。
本篇简单介绍 Spring Cache 的基本使用方式,下一篇将会从源码进行分析 Spring Cache 的基本工作原理 Spring Cache 原理解析。