spring cache 使用指南

spring有一套和各種緩存的集成方式。類似於sl4j,你可以選擇log框架實現,也一樣可以實現緩存實現,比如ehcache,guava cache.

首先,緩存是爲了省略消耗時間的步驟,比如io。當我需要從數據庫查詢的數據幾乎沒有變化,或者變化很少的時候,我就沒必要每次都去數據庫裏拿數據了。大可以放到本地,直接取出來就可以了。這時候需要注意的是數據一致性問題,緩存的數據是否被更改了,數據是否有效。


名字

位置

描述

示例

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

條件緩存
@Caching
自定義緩存註解
示例

   private final CacheOperationContextcontext;
  

   private final Object key;
  

   public CachePutRequest(CacheOperationContext context, Objectkey) {
     

      this.context = context;
     

      this.key = key;
  

   }
  

   public void apply(Object result) {
     

      if (this.context.canPutToCache(result)) {
 

        for (Cache cache : this.context.getCaches()) {
           

            cache.put(this.key, result); //保存key,value,cacheEhCacheguavaCacheredisCache
        }
     

      }
   }
}

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@sincespring 3.2spring4將此類更新到JCache 0.11版本;

另外,因爲我們在應用中並不是使用一個Cache,而是每種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"/>  // NoOpCache

</bean>  

當我們調用cacheManager.getCache(cacheName) 時,會先從第一個cacheManager中查找有沒有cacheNamecache,如果沒有接着查找第二個,如果最後找不到,因爲fallbackToNoOpCache=true,那麼將返回一個NOPCache否則返回null

除了GuavaCacheManager之外,其他Cache都支持Spring事務的,即如果事務回滾了,Cache的數據也會移除掉。

Spring不進行Cache的緩存策略的維護,這些都是由底層Cache自己實現,Spring只是提供了一個Wrapper,提供一套對外一致的API

示例

需要添加Ehcache依賴,具體依賴輕參考pom.xml

 

SpringCacheTest.java

Java代碼  

@Test  

public void test() throws IOException {  

    //創建底層Cache  

    net.sf.ehcache.CacheManager ehcacheManager  

            = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  

  

    //創建SpringCacheManager  

    EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager();  

    //設置底層的CacheManager  

    cacheCacheManager.setCacheManager(ehcacheManager);  

  

    Long id = 1L;  

    User user = new User(id, "zhang""[email protected]");  

  

    //根據緩存名字獲取Cache  

    Cache cache = cacheCacheManager.getCache("user");  

    //往緩存寫數據  

    cache.put(id, user);  

    //從緩存讀數據  

    Assert.assertNotNull(cache.get(id, User.class));  

}  

此處直接使用Spring提供的API進行操作;我們也可以通過xml/註解方式配置到spring容器;

 

xml風格的(spring-cache.xml):

Java代碼  

<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  

    <property name="configLocation" value="classpath:ehcache.xml"/>  

</bean>  

  

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  

    <property name="cacheManager" ref="ehcacheManager"/>  

    <property name="transactionAware" value="true"/>  

</bean>  

spring提供EhCacheManagerFactoryBean來簡化ehcache cacheManager的創建,這樣注入configLocation,會自動根據路徑從classpath下找,比編碼方式簡單多了,然後就可以從spring容器獲取cacheManager進行操作了。此處的transactionAware表示是否事務環繞的,如果true,則如果事務回滾,緩存也回滾,默認false

 

 

註解風格的(AppConfig.java):

Java代碼  

@Bean  

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);  

    }  

}  

和編程方式差不多就不多介紹了。

另外,除了這些默認的Cache之外,我們可以寫自己的Cache實現;而且即使不用之後的Spring Cache註解,我們也儘量使用Spring Cache API進行Cache的操作,如果要替換底層Cache也是非常方便的。到此基本的Cache API就介紹完了,接下來我們來看看使用Spring Cache註解來簡化Cache的操作。

 

XML風格的(spring-cache.xml):

Java代碼  

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

另外還可以指定一個 key-generator,即默認的key生成策略,後邊討論;

 

 

註解風格的(AppConfig.java):

Java代碼  

@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,然後注入需要的cacheManagerkeyGenerator;從spring4開始默認的keyGeneratorSimpleKeyGenerator

 

應用到寫數據的方法上,如新增/修改方法,調用方法時會自動把相應的數據放入緩存: 

Java代碼  

@CachePut(value = "user", key = "#user.id")  

public User save(User user) {  

    users.add(user);  

    return user;  

}  

即調用該方法時,會把user.id作爲key,返回值作爲value放入緩存;

 

@CachePut註解:

Java代碼  

public @interface CachePut {  

    String[] value();              //緩存的名字,可以把數據寫到多個緩存  

    String key() default "";       //緩存key,如果不指定將使用默認的KeyGenerator生成,後邊介紹  

    String condition() default ""//滿足緩存條件的數據纔會放入緩存,condition在調用方法之前和之後都會判斷  

    String unless() default "";    //用於否決緩存更新的,不像condition,該表達只在方法執行之後判斷,此時可以拿到返回值result進行判斷了  

}  

 

即應用到移除數據的方法上,如刪除方法,調用方法時會從緩存中移除相應的數據:

Java代碼  

@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註解:

Java代碼  

public @interface CacheEvict {  

    String[] value();                        //請參考@CachePut  

    String key() default "";                 //請參考@CachePut  

    String condition() default "";           //請參考@CachePut  

    boolean allEntries() default false;      //是否移除所有數據  

    boolean beforeInvocation() default false;//是調用方法之前移除/還是調用之後移除  

 

應用到讀取數據的方法上,即可緩存的方法,如查找方法:先從緩存中讀取,如果沒有再調用方法獲取數據,然後把數據添加到緩存中:

Java代碼  

@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註解:

Java代碼  

public @interface Cacheable {  

    String[] value();             //請參考@CachePut  

    String key() default "";      //請參考@CachePut  

    String condition() default "";//請參考@CachePut  

    String unless() default "";   //請參考@CachePut    

 

Java代碼  

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官方文檔:

通過這些數據我們可能實現比較複雜的緩存邏輯了,後邊再來介紹。

 

Key生成器

如果在Cache註解上沒有指定key的話@CachePut(value= "user"),會使用KeyGenerator進行生成一個key 

Java代碼  

public interface KeyGenerator {  

    Object generate(Object target, Method method, Object... params);  

}  

默認提供了DefaultKeyGenerator生成器(Spring4之後使用SimpleKeyGenerator): 

Java代碼  

@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生成器,然後通過xml風格的<cache:annotation-drivenkey-generator=""/>或註解風格的CachingConfigurer中指定keyGenerator 

 

根據運行流程,如下@Cacheable將在執行方法之前( #result還拿不到返回值)判斷condition,如果返回true,則查緩存; 

Java代碼  

@Cacheable(value = "user", key = "#id", condition = "#id lt 10")  

public User conditionFindById(final Long id)  

 

根據運行流程,如下@CachePut將在執行完方法後(#result就能拿到返回值了)判斷condition,如果返回true,則放入緩存; 

Java代碼  

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

public User conditionSave(final User user)   

  

根據運行流程,如下@CachePut將在執行完方法後(#result就能拿到返回值了)判斷unless,如果返回false,則放入緩存;(即跟condition相反)

Java代碼  

@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")  

    public User conditionSave2(final User user)   

 

根據運行流程,如下@CacheEvict beforeInvocation=false表示在方法執行之後調用(#result能拿到返回值了);且判斷condition,如果返回true,則移除緩存;

Java代碼  

@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")  

public User conditionDelete(final User user)   

 

有時候我們可能組合多個Cache註解使用;比如用戶新增成功後,我們要添加id-->userusername--->useremail--->user的緩存;此時就需要@Caching組合多個註解標籤了。

 

如用戶新增成功後,添加id-->userusername--->useremail--->user到緩存; 

Java代碼  

@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定義如下: 

Java代碼  

public @interface Caching {  

    Cacheable[] cacheable() default {}; //聲明多個@Cacheable  

    CachePut[] put() default {};        //聲明多個@CachePut  

    CacheEvict[] evict() default {};    //聲明多個@CacheEvict  

}  

 

比如之前的那個@Caching組合,會讓方法上的註解顯得整個代碼比較亂,此時可以使用自定義註解把這些註解組合到一個註解中,如: 

Java代碼  

@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 {  

}  

 

這樣我們在方法上使用如下代碼即可,整個代碼顯得比較乾淨。 

Java代碼  

@UserSaveCache  

public User save(User user)  

  

新增/修改數據時往緩存中寫 

Java代碼  

@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)  

 

Java代碼  

@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)   

刪除數據時從緩存中移除

Java代碼  

@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)   

Java代碼  

@CacheEvict(value = "user", allEntries = true)  

 public void deleteAll()  

 

查找時從緩存中讀

Java代碼  

@Caching(  

        cacheable = {  

                @Cacheable(value = "user", key = "#id")  

        }  

)  

public User findById(final Long id)   

 

Java代碼  

@Caching(  

         cacheable = {  

                 @Cacheable(value = "user", key = "#username")  

         }  

 )  

 public User findByUsername(final String username)   

 

Java代碼  

@Caching(  

          cacheable = {  

                  @Cacheable(value = "user", key = "#email")  

          }  

  )  

  public User findByEmail(final String email)  

  問題及解決方案

一、比如findByUsername時,不應該只放username-->user,應該連同id--->useremail--->user一起放入;這樣下次如果按照id查找直接從緩存中就命中了;這需要根據之前的運行流程改造CacheAspectSupport 

Java代碼  

// 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));  

}  

改爲:

Java代碼  

Collection<CacheOperationContext> cacheOperationContexts = contexts.get(CacheableOperation.class);  

if (!cacheOperationContexts.isEmpty()) {  

    result = findCachedResult(cacheOperationContexts);  

}  

然後就可以通過如下代碼完成想要的功能: 

Java代碼  

@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表達式:

Java代碼  

@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)  

 或更復雜的直接調用目標對象的方法進行操作(如只有修改了某個數據才從緩存中清除,比如菜單數據的緩存,只有修改了關鍵數據時才清空菜單對應的權限數據) 

Java代碼  

@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)   

Java代碼  

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靜態類中就可以解決這個問題了:

Java代碼  

@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--->userusername---->useremail--->user;更好的方式可能是:id--->userusername--->idemail--->id;保證user只存一份;如:

Java代碼  

@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")  

public void save(User user)   

Java代碼  

@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進行緩存抽象。

https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-cache

 

 

這裏採用guava cache作爲本地緩存。將guava cache註冊到cacheManger裏就可以調用了。

1.配置cacheManger

首先針對要緩存的類型,配置緩存策略。這裏設置最大緩存數量和緩存過期時間

public static final String HOTEL_POSTION = "hotel_position"//cache key

@Value("${cache.guavaCache.hotelPosition.maxSize}")

private longhotelPositionMaxSize;

@Value("${cache.guavaCache.hotelPosition.duration}")

private longhotelPositionDuration;

private GuavaCache buildHotelPositionCache(){

        returnnew GuavaCache(HOTEL_POSTION,

                CacheBuilder.newBuilder()

                        .recordStats()

                        .maximumSize(hotelPositionMaxSize)

                        .expireAfterWrite(hotelPositionDuration,TimeUnit.DAYS)

                        .build());

}

將剛纔創建的緩存策略添加到cacheManger:

    @Bean

    publicCacheManager cacheManager() {

        SimpleCacheManager manager = new SimpleCacheManager();

        List list = newArrayList();

        list.add(buildHotelPositionCache());

        manager.setCaches(  list );

        returnmanager;

    }

2.配置要緩存的方法

在需要使用這個緩存的地方,增加一行註解

@Cacheable(value = CacheManagementConfig.HOTEL_POSTION, key = "{#hotelId}", condition = "", unless = "!#result.isSuccessful()")

    publicBaseDomainResponse<HotelPosition> getHotelPosition(int hotelId, String apiToken) {

        //......

    }

@Cacheable表示這個方法要被緩存

value string,表示這個方法緩存的唯一性標識,即這方法緩存的key。語法爲SpEL. 爲配置文件中的緩存池名稱 key 爲鍵名

key String,表示每條請求緩存的key,即如果key相同,則返回緩存中對應的數據

condition boolean,可以額外添加緩存的條件.語法爲SpEL.

unless boolean, 配置哪些條件下的記錄不緩存。語法爲SpEL.

result表示return的這個對象,可以同result來調用這個對象的屬性,比如isSuccessful()就是我返回對象的一個方法。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章