zoukankan      html  css  js  c++  java
  • 基于Redis的Spring cache 缓存介绍

    Cache API及默认提供的实现

    Spring提供的核心Cache接口: 

    package org.springframework.cache;  
      
    public interface Cache {  
        String getName();  //缓存的名字  
        Object getNativeCache(); //得到底层使用的缓存,如Ehcache  
        ValueWrapper get(Object key); //根据key得到一个ValueWrapper,然后调用其get方法获取值  
        <T> T get(Object key, Class<T> type);//根据key,和value的类型直接获取value  
        void put(Object key, Object value);//往缓存放数据  
        void evict(Object key);//从缓存中移除key对应的缓存  
        void clear(); //清空缓存  
      
        interface ValueWrapper { //缓存值的Wrapper  
            Object get(); //得到真实的value  
            }  
    }   

    提供了缓存操作的读取/写入/移除方法; 

    默认提供了如下实现:

    ConcurrentMapCache:使用java.util.concurrent.ConcurrentHashMap实现的Cache;

    GuavaCache:对Guava com.google.common.cache.Cache进行的Wrapper,需要Google Guava 12.0或更高版本,@since spring 4;

    EhCacheCache:使用Ehcache实现

    JCacheCache:对javax.cache.Cache进行的wrapper,@since spring 3.2;spring4将此类更新到JCache 0.11版本; 

    另外,因为我们在应用中并不是使用一个Cache,而是多个,因此Spring还提供了CacheManager抽象,用于缓存的管理:  

    package org.springframework.cache;  
    import java.util.Collection;  
    public interface CacheManager {  
        Cache getCache(String name); //根据Cache名字获取Cache   
        Collection<String> getCacheNames(); //得到所有Cache的名字  

    默认提供的实现: 

    ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理ConcurrentMapCache;

    GuavaCacheManager;

    EhCacheCacheManager/EhCacheManagerFactoryBean;

    JCacheCacheManager/JCacheManagerFactoryBean;

     另外还提供了CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache,如

    <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">  
        <property name="cacheManagers">  
            <list>  
                <ref bean="ehcacheManager"/>  
                <ref bean="jcacheManager"/>  
            </list>  
        </property>  
        <property name="fallbackToNoOpCache" value="true"/>  
    </bean>   

    当我们调用cacheManager.getCache(cacheName) 时,会先从第一个cacheManager中查找有没有cacheName的cache,如果没有接着查找第二个,如果最后找不到,因为fallbackToNoOpCache=true,那么将返回一个NOP的Cache否则返回null。

    除了GuavaCacheManager之外,其他Cache都支持Spring事务的,即如果事务回滚了,Cache的数据也会移除掉。 

    Spring不进行Cache的缓存策略的维护,这些都是由底层Cache自己实现,Spring只是提供了一个Wrapper,提供一套对外一致的API。 

    demo

    依赖包安装

    <!-- redis cache related.....start -->  
    <dependency>  
        <groupId>org.springframework.data</groupId>  
        <artifactId>spring-data-redis</artifactId>  
        <version>1.6.0.RELEASE</version>  
    </dependency>  
    <dependency>  
        <groupId>redis.clients</groupId>  
        <artifactId>jedis</artifactId>  
        <version>2.7.3</version>  
    </dependency>  
    <!-- redis cache related.....end -->

    定义实体类、服务类和相关配置文件

    Account.java

    package cacheOfAnno; 
     
    public class Account { 
      private int id; 
      private String name; 
      
      public Account(String name) { 
        this.name = name; 
      } 
      public int getId() { 
        return id; 
      } 
      public void setId(int id) { 
        this.id = id; 
      } 
      public String getName() { 
        return name; 
      } 
      public void setName(String name) { 
        this.name = name; 
      } 
    } 
    AccountService.java
    package cacheOfAnno; 
     
    import org.springframework.cache.annotation.CacheEvict; 
    import org.springframework.cache.annotation.Cacheable; 
     
    public class AccountService { 
      @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache 
      public Account getAccountByName(String userName) { 
        // 方法内部实现不考虑缓存逻辑,直接实现业务
        System.out.println("real query account."+userName); 
        return getFromDB(userName); 
      } 
      
      private Account getFromDB(String acctName) { 
        System.out.println("real querying db..."+acctName); 
        return new Account(acctName); 
      } 
    }

    注意,此类的 getAccountByName 方法上有一个注释 annotation,即 @Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。

    好,因为加入了 spring,所以我们还需要一个 spring 的配置文件来支持基于注释的缓存。

    Spring-cache-anno.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
           xmlns:cache="http://www.springframework.org/schema/cache"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                            http://www.springframework.org/schema/cache
                            http://www.springframework.org/schema/cache/spring-cache-4.2.xsd">
    
        <!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,另外,该注解一定要声明在spring主配置文件中才会生效 -->
        <cache:annotation-driven cache-manager="cacheManager" key-generator="workingKeyGenerator"/>
    
        <!-- redis 相关配置 -->
        <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
            <!--<property name="maxIdle" value="${redis.maxIdle}" />-->
            <!--<property name="maxWaitMillis" value="${redis.maxWait}" />-->
            <!--<property name="testOnBorrow" value="${redis.testOnBorrow}" />-->
        </bean>
    
        <bean id="JedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
              p:host-name="${redis.ip}" p:port="${redis.port}" p:pool-config-ref="poolConfig"/>
    
        <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
            <property name="connectionFactory" ref="JedisConnectionFactory" />
        </bean>
    
        <!-- spring自己的缓存管理器,这里定义了缓存位置名称 ,即注解中的value -->
        <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
            <property name="caches">
                <set>
                    <bean
                            class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
                            p:name="default" />
                    <bean class="org.springframework.data.redis.cache.RedisCache">
                        <constructor-arg index="0" name="name" value="accountCache"/>
                        <constructor-arg index="1"><null/></constructor-arg>
                        <constructor-arg index="2" name="redisOperations" ref="redisTemplate"/>
                        <constructor-arg index="3" name="expiration" value="100"/>
                    </bean>
                </set>
            </property>
        </bean>
        <!--使用自定义key generator-->
        <bean id="workingKeyGenerator" class="com.cms.tzyy.common.cache.WorkingKeyGenerator" />
        <!--默认使用的key generator-->
        <!--<bean id="workingKeyGenerator" class="org.springframework.cache.interceptor.SimpleKeyGenerator" />-->
    </beans> 

    注意这个 spring 配置文件有一个关键的支持缓存的配置项:<cache:annotation-driven />,这个配置项缺省使用了一个名字叫 cacheManager 的缓存管理器,这个缓存管理器有一个 spring 的缺省实现,即 org.springframework.cache.support.SimpleCacheManager,这个缓存管理器实现了我们刚刚自定义的缓存管理器的逻辑,它需要配置一个属性 caches,即此缓存管理器管理的缓存集合,除了缺省的名字叫 default 的缓存(使用了缺省的内存存储方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一个内存缓存实现方案),我们还自定义了一个名字叫 accountCache 的缓存。

    OK,现在我们具备了测试条件,测试代码如下:

    Main.java
    package cacheOfAnno; 
     
    import org.springframework.context.ApplicationContext; 
    import org.springframework.context.support.ClassPathXmlApplicationContext; 
     
    public class Main { 
      public static void main(String[] args) { 
        ApplicationContext context = new ClassPathXmlApplicationContext( 
           "spring-cache-anno.xml");// 加载 spring 配置文件
        
        AccountService s = (AccountService) context.getBean("accountServiceBean"); 
        // 第一次查询,应该走数据库
        System.out.print("first query..."); 
        s.getAccountByName("somebody"); 
        // 第二次查询,应该不查数据库,直接返回缓存的值
        System.out.print("second query..."); 
        s.getAccountByName("somebody"); 
        System.out.println(); 
      } 
    } 

    上面的测试代码主要进行了两次查询,第一次应该会查询数据库,第二次应该返回缓存,不再查数据库,我们执行一下,看看结果。

    执行结果

    first query...real query account.somebody// 第一次查询
    real querying db...somebody// 对数据库进行了查询
    second query...// 第二次查询,没有打印数据库查询日志,直接返回了缓存中的结果 

    可以看出我们设置的基于注释的缓存起作用了,而在 AccountService.java 的代码中,我们没有看到任何的缓存逻辑代码,只有一行注释:@Cacheable(value="accountCache"),就实现了基本的缓存方案。

    Cache注解

    启用Cache注解

    XML风格的:

    <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>  

    另外还可以指定一个 key-generator,即默认的key生成策略,后边讨论; 

    注解风格的:

    @Configuration  
    @ComponentScan(basePackages = "com.sishuok.spring.service")  
    @EnableCaching(proxyTargetClass = true)  
    public class AppConfig implements CachingConfigurer {  
        @Bean  
        @Override  
        public CacheManager cacheManager() {  
      
            try {  
                net.sf.ehcache.CacheManager ehcacheCacheManager  
                        = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  
      
                EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);  
                return cacheCacheManager;  
            } catch (IOException e) {  
                throw new RuntimeException(e);  
            }  
        }  
      
        @Bean  
        @Override  
        public KeyGenerator keyGenerator() {  
            return new SimpleKeyGenerator();  
        }  
    }   

    1、使用@EnableCaching启用Cache注解支持;

    2、实现CachingConfigurer,然后注入需要的cacheManager和keyGenerator;从spring4开始默认的keyGenerator是SimpleKeyGenerator; 

    @CachePut 

    应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存: 

    @CachePut(value = "user", key = "#user.id")  
    public User save(User user) {  
        users.add(user);  
        return user;  
    } 

    即调用该方法时,会把user.id作为key,返回值作为value放入缓存; 

    @CachePut注解: 

    public @interface CachePut {  
        String[] value();              //缓存的名字,可以把数据写到多个缓存  
        String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  
        String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  
        String unless() default "";    //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了  

    @CacheEvict 

    即应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据: 

    @CacheEvict(value = "user", key = "#user.id") //移除指定key的数据  
    public User delete(User user) {  
        users.remove(user);  
        return user;  
    }  
    @CacheEvict(value = "user", allEntries = true) //移除所有数据  
    public void deleteAll() {  
        users.clear();  
    } 

    @CacheEvict注解: 

    public @interface CacheEvict {  
        String[] value();                        //请参考@CachePut  
        String key() default "";                 //请参考@CachePut  
        String condition() default "";           //请参考@CachePut  
        boolean allEntries() default false;      //是否移除所有数据  
        boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除 

    @Cacheable

    应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中:

    @Cacheable(value = "user", key = "#id")  
     public User findById(final Long id) {  
         System.out.println("cache miss, invoke find by id, id:" + id);  
         for (User user : users) {  
             if (user.getId().equals(id)) {  
                 return user;  
             }  
         }  
         return null;  
     } 

    @Cacheable注解:

    public @interface Cacheable {  
        String[] value();             //请参考@CachePut  
        String key() default "";      //请参考@CachePut  
        String condition() default "";//请参考@CachePut  
        String unless() default "";   //请参考@CachePut   

    运行流程

    1. 首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有  
    2. 接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)  
    3. 如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)  
    4. 如果没有找到缓存,那么调用实际的API,把结果放入result  
    5. 如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests  
    6. 执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);  
    7. 执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有  

    流程中需要注意的就是2/3/4步:

    如果有@CachePut操作,即使有@Cacheable也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut和@Cacheable;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景);不过个人感觉还是不要考虑这个好,让用户来决定如何使用,否则一会介绍的场景不能满足。

    提供的SpEL上下文数据

    Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:

    名字 位置 描述 示例

    methodName

    root对象

    当前被调用的方法名

    #root.methodName

    method

    root对象

    当前被调用的方法

    #root.method.name

    target

    root对象

    当前被调用的目标对象

    #root.target

    targetClass

    root对象

    当前被调用的目标对象类

    #root.targetClass

    args

    root对象

    当前被调用的方法的参数列表

    #root.args[0]

    caches

    root对象

    当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache

    #root.caches[0].name

    argument name

    执行上下文

    当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数

    #user.id

    result

    执行上下文

    方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false)

    #result

    通过这些数据我们可能实现比较复杂的缓存逻辑了,后边再来介绍。 

    Key生成器

    如果在Cache注解上没有指定key的话@CachePut(value = "user"),会使用KeyGenerator进行生成一个key: 

    public interface KeyGenerator {  
        Object generate(Object target, Method method, Object... params);  
    }   

    默认提供了DefaultKeyGenerator生成器(Spring 4之后使用SimpleKeyGenerator): 

    @Override  
    public Object generate(Object target, Method method, Object... params) {  
        if (params.length == 0) {  
            return SimpleKey.EMPTY;  
        }  
        if (params.length == 1 && params[0] != null) {  
            return params[0];  
        }  
        return new SimpleKey(params);  
    } 

    即如果只有一个参数,就使用参数作为key,否则使用SimpleKey作为key。 

    我们也可以自定义自己的key生成器(参考:https://marschall.github.io/2017/10/01/better-spring-cache-key-generator.html),然后通过xml风格的<cache:annotation-driven key-generator=""/>或注解风格的CachingConfigurer中指定keyGenerator。 

    条件缓存

    根据运行流程,如下@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; 

    @Cacheable(value = "user", key = "#id", condition = "#id lt 10")  
    public User conditionFindById(final Long id)   

    根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; 

    @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")  
    public User conditionSave(final User user)     

    根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)

    @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")  
        public User conditionSave2(final User user)    
    根据运行流程,如下@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;
    @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")  
    public User conditionDelete(final User user)    

    @Caching

    有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id-->user;username--->user;email--->user的缓存;此时就需要@Caching组合多个注解标签了。 

    如用户新增成功后,添加id-->user;username--->user;email--->user到缓存; 

    @Caching(  
            put = {  
                    @CachePut(value = "user", key = "#user.id"),  
                    @CachePut(value = "user", key = "#user.username"),  
                    @CachePut(value = "user", key = "#user.email")  
            }  
    )  
    public User save(User user) {    

    @Caching定义如下: 

    public @interface Caching {  
        Cacheable[] cacheable() default {}; //声明多个@Cacheable  
        CachePut[] put() default {};        //声明多个@CachePut  
        CacheEvict[] evict() default {};    //声明多个@CacheEvict  
    }   

    自定义缓存注解

    比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如: 

    @Caching(  
            put = {  
                    @CachePut(value = "user", key = "#user.id"),  
                    @CachePut(value = "user", key = "#user.username"),  
                    @CachePut(value = "user", key = "#user.email")  
            }  
    )  
    @Target({ElementType.METHOD, ElementType.TYPE})  
    @Retention(RetentionPolicy.RUNTIME)  
    @Inherited  
    public @interface UserSaveCache {  
    }  

    这样我们在方法上使用如下代码即可,整个代码显得比较干净。 

    @UserSaveCache  
    public User save(User user)    

    示例

    新增/修改数据时往缓存中写 

    @Caching(  
            put = {  
                    @CachePut(value = "user", key = "#user.id"),  
                    @CachePut(value = "user", key = "#user.username"),  
                    @CachePut(value = "user", key = "#user.email")  
            }  
    )  
    public User save(User user)  
    @Caching(  
            put = {  
                    @CachePut(value = "user", key = "#user.id"),  
                    @CachePut(value = "user", key = "#user.username"),  
                    @CachePut(value = "user", key = "#user.email")  
            }  
    )  
    public User update(User user)    
    删除数据时从缓存中移除
    @Caching(  
            evict = {  
                    @CacheEvict(value = "user", key = "#user.id"),  
                    @CacheEvict(value = "user", key = "#user.username"),  
                    @CacheEvict(value = "user", key = "#user.email")  
            }  
    )  
    public User delete(User user)    
    @CacheEvict(value = "user", allEntries = true)  
     public void deleteAll()   
    查找时从缓存中读
    @Caching(  
            cacheable = {  
                    @Cacheable(value = "user", key = "#id")  
            }  
    )  
    public User findById(final Long id)    
    @Caching(  
             cacheable = {  
                     @Cacheable(value = "user", key = "#username")  
             }  
     )  
     public User findByUsername(final String username) 
    @Caching(  
              cacheable = {  
                      @Cacheable(value = "user", key = "#email")  
              }  
      )  
      public User findByEmail(final String email)   

    基本原理

    和 spring 的事务管理类似,spring cache 的关键原理就是 spring AOP,通过 spring AOP,其实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。我们来看一下下面这个图:

    原始方法调用图

    上图显示,当客户端“Calling code”调用一个普通类 Plain Object 的 foo() 方法的时候,是直接作用在 pojo 类自身对象上的,客户端拥有的是被调用者的直接的引用。

    而 Spring cache 利用了 Spring AOP 的动态代理技术,即当客户端尝试调用 pojo 的 foo()方法的时候,给他的不是 pojo 自身的引用,而是一个动态生成的代理类

    动态代理调用图

    如上图所示,这个时候,实际客户端拥有的是一个代理的引用,那么在调用 foo() 方法的时候,会首先调用 proxy 的 foo() 方法,这个时候 proxy 可以整体控制实际的 pojo.foo() 方法的入参和返回值,比如缓存结果,比如直接略过执行实际的 foo() 方法等,都是可以轻松做到的。

    注意和限制

    CacheManager 必须设置缓存过期时间,否则缓存对象将永不过期,这样做的原因是避免一些野数据“永久保存”。此外,设置缓存过期时间也有助于资源利用最大化,因为缓存里保留的永远是热点数据。

    缓存适用于读多写少的场合,查询时缓存命中率很低、写操作很频繁等场景不适宜用缓存。

    基于 proxy 的 spring aop 带来的内部调用问题

    上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。

    清单 28. AccountService.java
    public Account getAccountByName2(String userName) {
      return this.getAccountByName(userName);
    }
     
    @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache 
    public Account getAccountByName(String userName) {
      // 方法内部实现不考虑缓存逻辑,直接实现业务
      return getFromDB(userName); 

    上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效

    清单 29. Main.java
    public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext(
         "spring-cache-anno.xml");// 加载 spring 配置文件
      
      AccountService s = (AccountService) context.getBean("accountServiceBean"); 
      
      s.getAccountByName2("someone");
      s.getAccountByName2("someone");
      s.getAccountByName2("someone");
    } 
    清单 30. 运行结果
    real querying db...someone 
    real querying db...someone 
    real querying db...someone

    可见,结果是每次都查询数据库,缓存没起作用。要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。

    @CacheEvict 的可靠性问题

    我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存不会被清空。我们演示一下

    清单 31. AccountService.java
    @CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 缓存
    public void reload() { 
      throw new RuntimeException(); 
    }

    注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。

    清单 32. Main.java
    public static void main(String[] args) { 
      ApplicationContext context = new ClassPathXmlApplicationContext( 
         "spring-cache-anno.xml");// 加载 spring 配置文件
      
      AccountService s = (AccountService) context.getBean("accountServiceBean"); 
      
      s.getAccountByName("someone"); 
      s.getAccountByName("someone"); 
      try { 
        s.reload(); 
      } catch (Exception e) { 
      } 
      s.getAccountByName("someone"); 
    }

    上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。

    清单 33. 运行结果
    real querying db...someone
     

    和预期一样。那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。

    清单 34. AccountService.java
    @CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true)
    // 清空 accountCache 缓存
    public void reload() { 
      throw new RuntimeException(); 
    }

    注意上面的代码,我们在 @CacheEvict 注释中加了 beforeInvocation 属性,确保缓存被清空。

    执行相同的测试代码

    清单 35. 运行结果
    real querying db...someone 
    real querying db...someone

    这样,第一次和第三次都从数据库取数据了,缓存清空有效。

    非 public 方法问题

    和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制。

    问题及解决方案

    一、比如findByUsername时,不应该只放username-->user,应该连同id--->user和email--->user一起放入;这样下次如果按照id查找直接从缓存中就命中了;这需要根据之前的运行流程改造CacheAspectSupport: 

    // We only attempt to get a cached result if there are no put requests  
    if (cachePutRequests.isEmpty() && contexts.get(CachePutOperation.class).isEmpty()) {  
        result = findCachedResult(contexts.get(CacheableOperation.class));  
    }  
    改为:
    Collection<CacheOperationContext> cacheOperationContexts = contexts.get(CacheableOperation.class);  
    if (!cacheOperationContexts.isEmpty()) {  
        result = findCachedResult(cacheOperationContexts);  
    } 

    然后就可以通过如下代码完成想要的功能: 

    @Caching(  
            cacheable = {  
                    @Cacheable(value = "user", key = "#username")  
            },  
            put = {  
                    @CachePut(value = "user", key = "#result.id", condition = "#result != null"),  
                    @CachePut(value = "user", key = "#result.email", condition = "#result != null")  
            }  
    )  
    public User findByUsername(final String username) {  
        System.out.println("cache miss, invoke find by username, username:" + username);  
        for (User user : users) {  
            if (user.getUsername().equals(username)) {  
                return user;  
            }  
        }  
        return null;  
    } 

    二、缓存注解会让代码看上去比较乱;应该使用自定义注解把缓存注解提取出去; 

    三、往缓存放数据/移除数据是有条件的,而且条件可能很复杂,考虑使用SpEL表达式:

    @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true)  
    public void conditionUpdate(User user) 
     或更复杂的直接调用目标对象的方法进行操作(如只有修改了某个数据才从缓存中清除,比如菜单数据的缓存,只有修改了关键数据时才清空菜单对应的权限数据) 
    @Caching(  
            evict = {  
                    @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)  
            }  
    )  
    public void conditionUpdate(User user)    
    public boolean canEvict(Cache userCache, Long id, String username) {  
        User cacheUser = userCache.get(id, User.class);  
        if (cacheUser == null) {  
            return false;  
        }  
        return !cacheUser.getUsername().equals(username);  
    } 
    如上方式唯一不太好的就是缓存条件判断方法也需要暴露出去;而且缓存代码和业务代码混合在一起,不优雅;因此把canEvict方法移到一个Helper静态类中就可以解决这个问题了:
    @CacheEvict(value = "user", key = "#user.id", condition = "T(com.sishuok.spring.service.UserCacheHelper).canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)  
    public void conditionUpdate(User user)   

    四、其实对于:id--->user;username---->user;email--->user;更好的方式可能是:id--->user;username--->id;email--->id;保证user只存一份;如:

    @CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")  
    public void save(User user)   
    @Cacheable(value="cacheName", ley="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())")  
    public User findByUsername(String username) 

    五、使用Spring3.1注解 缓存 模糊匹配Evict的问题 

    缓存都是key-value风格的,模糊匹配本来就不应该是Cache要做的;而是通过自己的缓存代码实现; 

    六、spring cache的缺陷:例如有一个缓存存放 list<User>,现在你执行了一个 update(user)的方法,你一定不希望清除整个缓存而想替换掉update的元素

    这个在现有的抽象上没有很好的方案,可以考虑通过condition在之前的Helper方法中解决;当然不是很优雅。 

    也就是说Spring Cache注解还不是很完美,我认为可以这样设计:

    @Cacheable(cacheName = "缓存名称",key="缓存key/SpEL", value="缓存值/SpEL/不填默认返回值",  beforeCondition="方法执行之前的条件/SpEL", afterCondition="方法执行后的条件/SpEL", afterCache="缓存之后执行的逻辑/SpEL")

    value也是一个SpEL,这样可以定制要缓存的数据;afterCache定制自己的缓存成功后的其他逻辑。 

    当然Spring Cache注解对于大多数场景够用了,如果场景复杂还是考虑使用AOP吧;如果自己实现请考虑使用Spring Cache API进行缓存抽象。

     参考资料

    http://jinnianshilongnian.iteye.com/blog/2001040

    https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/

    http://blog.csdn.net/defonds/article/details/48716161

    https://marschall.github.io/2017/10/01/better-spring-cache-key-generator.html

  • 相关阅读:
    base64解码
    字典排序之后md5
    python 如何将列表多个字符串拼接成一个字符串
    Mongodb $setOnInsert操作符 和upsert:true
    为了不复制粘贴,我被逼着学会了JAVA爬虫
    10-序列化
    【开发笔记】- 停止MySQL正在执行的SQL语句
    【算法练习】- 百钱百鸡
    volatile与synchronized的区别与作用
    记录一次使用OSS而引发的关于maven的问题
  • 原文地址:https://www.cnblogs.com/junzi2099/p/8301796.html
Copyright © 2011-2022 走看看