1 分钟快速上手 Spring Cache

快速开始(从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 时,使用的是 SIMPLECacheType 所有枚举类型如下:

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 typeredis

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 原理解析

Supongo que te gusta

Origin juejin.im/post/7067090649245286408
Recomendado
Clasificación