基於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM