Spring的缓存机制及自定义缓存


Spring的缓存机制

Spring的缓存不是一种具体的缓存实现方案,它底层需要依赖EhCache、Guava等具体的缓存工具。应用程序只要面向Spring缓存API编程,应用底层的缓存实现可以在不同的缓存之间自由切换,应用程序无须任何改变,只需要对配置略作修改即可。 启用Spring缓存

(1) 创建一个有@Configuration注解的配置类,添加@EnableCaching注解开启缓存机制,并继承CachingConfigurerSupport自定义缓存(或xml配置:) 

(2) 针对不同的缓存实现配置对应的缓存管理器。

① spring-context-*.jar 包含了 cache 需要的类。

② 如果引入了spring-data-redis,要注意版本的问题。我使用spring 5.0.2 与 spring-data-redis 2.0.2.RELEASE是兼容的,但是跟spring-data-redis 2.1.9.RELEASE是有问题的(因为该版本使用的spring与项目的spring版本有冲突)。

③ 只有使用public定义的方法才可以被缓存,而private方法、protected 方法或者使用default 修饰符的方法都不能被缓存。 当在一个类上使用注解时,该类中每个公共方法的返回值都将被缓存到指定的缓存项中或者从中移除。

④ 命名空间:

xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"

Spring内置缓存实现的配置

Spring内置的缓存实现只是一种内存中的缓存,并非真正的缓存实现,因此通常指能用于简单的测试环境,不建议在实际项目中使用Spring内置的缓存实现。

Spring内置的缓存实现使用SimpleCacheManager作为缓存管理器,使用SimpleCacheManager配置缓存非常简单,直接在Spring容器中配置该Bean,然后通过驱动该缓存管理器执行setCaches()放来来设置缓存区即可。

SimpleCacheManager是一种内存中的缓存区,底层直接使用了JDK的ConcurrentMap来实现缓存,SimpleCacheManager使用了ConcurrentMapCacheFactoryBean作为缓存区,每个ConcurrentMapCacheFactoryBean配置一个缓存区。 xml配置:
<cache:annotation-driven cache-mangager="cacheManager"/>
<bean id="cacheManager" class="org.springframework.cache.suppot.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframeword.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> <bean class="org.springframeword.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="users"/> set> property> bean>

上面配置文件使用SimpleCacheManager配置了Spring内置的缓存管理器,并为该缓存管理器配置列两个缓存区:default和users————这些缓存区的名字很重要,后面使用注解驱动缓存时需要根据缓存区的名字来讲缓存数据放入指定缓存区中。

在实际应用中,开发者可以根据自己的需要,配置更多的缓存区,一般来说,应用有多少个组件需要缓存,程序就应该配置多少个缓存区。

Java配置:

@EnableCaching
@Configuration
public class SpringCacheConfig extends CachingConfigurerSupport {

    @Override
    public CacheManager cacheManager() {
        return simpleCacheManager();
    }

    @Bean
    public SimpleCacheManager simpleCacheManager(){
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List cacheList = new ArrayList();

        ConcurrentMapCacheFactoryBean defaultFactoryBean = new ConcurrentMapCacheFactoryBean();
        defaultFactoryBean.setName("default");
        cacheList.add(defaultFactoryBean.getObject());

        ConcurrentMapCacheFactoryBean usersFactoryBean = new ConcurrentMapCacheFactoryBean();
        usersFactoryBean.setName("users");
        cacheList.add(usersFactoryBean.getObject());

        cacheManager.setCaches(cacheList);
        return cacheManager;
    }
}

CachingConfigurerSupport类

cacheManager方法

指定使用哪一种缓存。Spring内置了SimpleCacheManager。

keyGenerator方法

指定默认的key生成方式。Spring内置了SimpleKeyGenerator。 

errorHandler方法

添加自定义缓存异常处理,实现CacheErrorHandler接口即可。

Spring内置了SimpleCacheErrorHandler。

缓存仅仅是为了业务更快地查询而存在的,如果因为缓存操作失败导致正常的业务流程失败,有点得不偿失了。因此需要开发者自定义CacheErrorHandler处理缓存读写的异常。

/**
 * 当缓存读写异常时,忽略异常
 */
public class IgnoreExceptionCacheErrorHandler implements CacheErrorHandler {

    private static final Logger log = LoggerFactory.getLogger(IgnoreExceptionCacheErrorHandler.class);

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
        log.error(exception.getMessage(), exception);
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
        log.error(exception.getMessage(), exception);
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        log.error(exception.getMessage(), exception);
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
        log.error(exception.getMessage(), exception);
    }
}

Spring的缓存注解

@Cacheable注解执行缓存

@Cacheable可用于修饰类或修饰方法,当使用@Cacheable修饰类时,用于告诉Spring在类级别上进行缓存————程序调用该类的实例的任何方法时都需要缓存,而且共享同一个缓存区;当使用@Cacheable修饰方法时,用于告诉Spring在方法级别上进行缓存————只有当程序调用该方法时才需要缓存。

  • 类级别的缓存:使用@Cacheable修饰类时,就可控制Spring在类级别进行缓存,这样当程序调用该类的任意方法时,只要传入的参数相同,Spring就会使用缓存。

    当程序第一次调用该类的实例的某个方法时,Spring缓存机制会将该方法返回的数据放入指定缓存区。以后程序调用该类的实例的任何方法时,只要传入的参数相同,Spring将不会真正执行该方法,而是直接利用缓存区中的数据

  • 方法级别的缓存:使用@Cacheable修饰方法时,就可控制Spring在方法级别进行缓存,这样当程序调用该方法时,只要传入的参数相同,Spring就会使用缓存。

在 @Cacheable 注解的使用中,共有 9 个属性供我们来使用,这 9 个属性分别是: value、 cacheNames、 key、 keyGenerator、 cacheManager、 cacheResolver、 condition、 unless、 sync。

1、value/cacheNames 属性

这两个属性代表的意义相同,根据@AliasFor注解就能看出来了。这两个属性都是用来指定缓存组件的名称,即将方法的返回结果放在哪个缓存中,属性定义为数组,可以指定多个缓存;

//这两种配置等价
@Cacheable(value = "user") //@Cacheable(cacheNames = "user")
User getUser(Integer id);

2、key属性

可以通过 key 属性来指定缓存数据所使用的的 key,默认使用的是方法调用传过来的参数作为 key。最终缓存中存储的内容格式为:Entry 形式。

(1) 如果请求没有参数:key=new SimpleKey();

(2) 如果请求有一个参数:key=参数的值

(3) 如果请求有多个参数:key=new SimpleKey(params); (你只要知道 key不会为空就行了)

key值的编写,可以使用 SpEL 表达式的方式来编写;除此之外,我们同样可以使用 keyGenerator 生成器的方式来指定 key,我们只需要编写一个 keyGenerator ,将该生成器注册到 IOC 容器即可。

名字 位置   描述 示例 
methodName root object  当前被调用的方法名  #root.method.name 
method  root object  当前被调用的方法  #root.methodName 
target  root object  当前被调用的目标对象  #root.target 
targetClass  root object  当前被调用的目标对象类  #root.targetClass 
args  root object 当前被调用的方法的参数列表  #root.args[0] 
caches  root object  当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”,“cache2”})),则有两个cache  #root.caches[0].name 
argument name  evaluation context  方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引;  #id、#p0、#a0 
result  evaluation context  方法执行后的返回值(仅当方法执行之后的判断有效,如’unless’、'cache put’的表达式 'cacheevict’的表达式beforeInvocation=false)  #result 

使用示例如下:

@Cacheable(value = "user",key = "#root.method.name")
User getUser(Integer id);

3、keyGenerator 属性

key 的生成器。如果觉得通过参数的方式来指定比较麻烦,我们可以自己指定 key 的生成器的组件 id。key/keyGenerator属性:二选一使用。我们可以通过自定义配置类方式,将 keyGenerator 注册到 IOC 容器来使用。

@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
    return new KeyGenerator(){

        @Override
        public Object generate(Object target, Method method, Object... params) {
            return method.getName()+ Arrays.asList(params).toString();
        }
    };
}

4、cacheManager 属性

该属性,用来指定缓存管理器。针对不同的缓存技术,需要实现不同的 cacheManager,Spring 也为我们定义了如下的一些 cacheManger 实现:

CacheManger 描述
SimpleCacheManager 使用简单的Collection来存储缓存,主要用于测试
ConcurrentMapCacheManager 使用ConcurrentMap作为缓存技术(默认)
NoOpCacheManager 测试用
EhCacheCacheManager 使用EhCache作为缓存技术,以前在hibernate的时候经常用
GuavaCacheManager 使用google guava的GuavaCache作为缓存技术
HazelcastCacheManager 使用Hazelcast作为缓存技术
JCacheCacheManager 使用JCache标准的实现作为缓存技术,如Apache Commons JCS
RedisCacheManager 使用Redis作为缓存技术

5、cacheResolver 属性

该属性,用来指定缓存管理器。使用配置同 cacheManager 类似。(cacheManager指定管理器/cacheResolver指定解析器 它俩也是二选一使用)。

6、condition 属性

条件判断属性,用来指定符合指定的条件下才可以缓存。也可以通过 SpEL 表达式进行设置。这个配置规则和上面表格中的配置规则是相同的。 

@Cacheable(value = "user",condition = "#id>0")//传入的 id 参数值>0才进行缓存
User getUser(Integer id);
@Cacheable(value = "user",condition = "#a0>1")//传入的第一个参数的值>1的时候才进行缓存
User getUser(Integer id);
@Cacheable(value = "user",condition = "#a0>1 and #root.methodName eq 'getUser'")//传入的第一个参数的值>1 且 方法名为 getUser 的时候才进行缓存
User getUser(Integer id);

7、unless 属性

unless属性,意为"除非"的意思。即只有 unless 指定的条件为 true 时,方法的返回值才不会被缓存。可以在获取到结果后进行判断。

@Cacheable(value = "user",unless = "#result == null")//当方法返回值为 null 时,就不缓存
User getUser(Integer id);
@Cacheable(value = "user",unless = "#a0 == 1")//如果第一个参数的值是1,结果不缓存
User getUser(Integer id);

8、sync 属性

该属性用来指定是否使用异步模式,该属性默认值为 false,默认为同步模式。异步模式指定 sync = true 即可,异步模式下 unless 属性不可用。

@Cacheput注解执行缓存

在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。

@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CachePut注解和@Cacheable注解,唯一不同的是:

  • @CachePut注解是先调用方法再将方法的返回值写入缓存,方法体是一定会执行的;
  • @Cacheable注解是先查询缓存,if 缓存==null,就查询数据库,然后将方法的返回值写入缓存

@CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

@CacheEvict注解清除缓存

@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。

当标记在一个类上时表示所有的方法的执行都会触发缓存的清除操作。

@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。

其中value、key和condition的语义与@Cacheable对应的属性类似。

  • value表示清除操作是发生在哪些Cache上的(对应Cache的名称);
  • key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;
  • condition表示清除操作发生的条件。

下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

1、allEntries

allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
  System.out.println("delete user by id: " + id);
}

2、beforeInvocation属性

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
  System.out.println("delete user by id: " + id);
}

@Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
     @CacheEvict(value = "cache3", allEntries = true) })
public User find(Integer id) {
  return null;
}

使用自定义注解

Spring允许我们在配置可缓存的方法时使用自定义的注解,前提是自定义的注解上必须使用对应的注解进行标注。如我们有如下这么一个使用@Cacheable进行标注的自定义注解。

自定义注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(value="users")
public @interface MyCacheable {


}

那么在我们需要缓存的方法上使用@MyCacheable进行标注也可以达到同样的效果。

@MyCacheable
public User findById(Integer id) {
  System.out.println("find user by id: " + id);
  User user = new User();
  user.setId(id);
  user.setName("Name" + id);
  return user;
}

@CacheConfig 公共的缓存配置

@CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)

@Cacheable的实现原理

Cacheable注解是典型的Spring AOP实现,在Spring里,aop可以简单的理解为代理(AspectJ除外),我们声明了@Cacheable的方法的类,都会被代理,在代理中,实现缓存的查询与设置操作。

众所周知,使用CgLib进行AOP动态代理是通过拦截器中的invoke方法执行的。

在这里找到了缓存方面的代理配置:

自定义缓存

使用EhCache

使用Redis

1、pom配置

spring 5.0.2.RELEASE。

spring-data-redis 2.0.2.RELEASE

jedis 2.10.2


<dependency>
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-redisartifactId>
    <version>2.0.2.RELEASEversion>
    <exclusions>

    exclusions>
dependency>

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>2.10.2version>
dependency>

2、配置RedisCacheManager