Spring Cache抽象詳解
緩存簡介
緩存,我的理解是:讓數據更接近于使用者;工作機制是:先從緩存中讀取數據,如果沒有再從慢速設備上讀取實際數據(數據也會存入緩存);緩存什么:那些經常讀取且不經常修改的數據/那些昂貴(CPU/IO)的且對于相同的請求有相同的計算結果的數據。如CPU--L1/L2--內存 --磁盤就是一個典型的例子,CPU需要數據時先從L1/L2中讀取,如果沒有到內存中找,如果還沒有會到磁盤上找。還有如用過Maven的朋友都應該知道,我們找依賴的時候,先從本機倉庫找,再從本地服務器倉庫找,最后到遠程倉庫服務器找;還有如京東的物流為什么那么快?他們在各個地都有分倉庫,如果該倉庫有貨物那么送貨的速度是非常快的。
緩存命中率
即從緩存中讀取數據的次數 與 總讀取次數的比率,命中率越高越好:
命中率 = 從緩存中讀取次數 / (總讀取次數[從緩存中讀取次數 + 從慢速設備上讀取的次數])
Miss率 = 沒有從緩存中讀取的次數 / (總讀取次數[從緩存中讀取次數 + 從慢速設備上讀取的次數])
這是一個非常重要的監控指標,如果做緩存一定要健康這個指標來看緩存是否工作良好;
緩存策略
Eviction policy
移除策略,即如果緩存滿了,從緩存中移除數據的策略;常見的有LFU、LRU、FIFO:
FIFO(First In First Out):先進先出算法,即先放入緩存的先被移除;
LRU(Least Recently Used):最久未使用算法,使用時間距離現在最久的那個被移除;
LFU(Least Frequently Used):最近最少使用算法,一定時間段內使用次數(頻率)最少的那個被移除;
TTL(Time To Live )
存活期,即從緩存中創建時間點開始直到它到期的一個時間段(不管在這個時間段內有沒有訪問都將過期)
TTI(Time To Idle)
空閑期,即一個數據多久沒被訪問將從緩存中移除的時間。
到此,基本了解了緩存的知識,在Java中,我們一般對調用方法進行緩存控制,比如我調用"findUserById(Long id)",那么我應該在調用這個方法之前先從緩存中查找有沒有,如果沒有再掉該方法如從數據庫加載用戶,然后添加到緩存中,下次調用時將會從緩存中獲取到數據。
自Spring 3.1起,提供了類似于@Transactional注解事務的注解Cache支持,且提供了Cache抽象;在此之前一般通過AOP實現;使用Spring Cache的好處:
提供基本的Cache抽象,方便切換各種底層Cache;
通過注解Cache可以實現類似于事務一樣,緩存邏輯透明的應用到我們的業務代碼上,且只需要更少的代碼就可以完成;
提供事務回滾時也自動回滾緩存;
支持比較復雜的緩存邏輯;
對于Spring Cache抽象,主要從以下幾個方面學習:
-
Cache API及默認提供的實現
</li> -
Cache注解
</li> -
實現復雜的Cache邏輯
</li> </ul>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
} }</pre>
提供了緩存操作的讀取/寫入/移除方法;
默認提供了如下實現:
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。
示例
需要添加Ehcache依賴,具體依賴輕參考pom.xml
@Test public void test() throws IOException { //創建底層Cache net.sf.ehcache.CacheManager ehcacheManager = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());
//創建Spring的CacheManager EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(); //設置底層的CacheManager cacheCacheManager.setCacheManager(ehcacheManager);
Long id = 1L; User user = new User(id, "zhang", "zhang@gmail.com");
//根據緩存名字獲取Cache Cache cache = cacheCacheManager.getCache("user"); //往緩存寫數據 cache.put(id, user); //從緩存讀數據 Assert.assertNotNull(cache.get(id, User.class)); }</pre>
此處直接使用Spring提供的API進行操作;我們也可以通過xml/注解方式配置到spring容器;
xml風格的(spring-cache.xml):
<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></pre>
spring提供EhCacheManagerFactoryBean來簡化ehcache cacheManager的創建,這樣注入configLocation,會自動根據路徑從classpath下找,比編碼方式簡單多了,然后就可以從 spring容器獲取cacheManager進行操作了。此處的transactionAware表示是否事務環繞的,如果true,則如果事務回滾,緩存也回滾,默認false。
注解風格的(AppConfig.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); } }</pre>
和編程方式差不多就不多介紹了。
另外,除了這些默認的Cache之外,我們可以寫自己的Cache實現;而且即使不用之后的Spring Cache注解,我們也盡量使用Spring Cache API進行Cache的操作,如果要替換底層Cache也是非常方便的。到此基本的Cache API就介紹完了,接下來我們來看看使用Spring Cache注解來簡化Cache的操作。
Cache注解
啟用Cache注解
XML風格的(spring-cache.xml):
<cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>
另外還可以指定一個 key-generator,即默認的key生成策略,后邊討論;
注解風格的(AppConfig.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(); } }</pre>
1、使用@EnableCaching啟用Cache注解支持;
2、實現CachingConfigurer,然后注入需要的cacheManager和keyGenerator;從spring4開始默認的keyGenerator是SimpleKeyGenerator;
應用到寫數據的方法上,如新增/修改方法,調用方法時會自動把相應的數據放入緩存:
@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(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(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官方文檔:
名字 位置 描述 示例 </tr>methodName
</td>root對象
</td>當前被調用的方法名
</td>
</td> </tr>#root.methodName
method
</td>root對象
</td>當前被調用的方法
</td>
</td> </tr>#root.method.name
target
</td>root對象
</td>當前被調用的目標對象
</td>
</td> </tr>#root.target
targetClass
</td>root對象
</td>當前被調用的目標對象類
</td>
</td> </tr>#root.targetClass
args
</td>root對象
</td>當前被調用的方法的參數列表
</td>
</td> </tr>#root.args[0]
caches
</td>root對象
</td>當前方法調用使用的緩存列表(如@Cacheable(value={"cache1", "cache2"})),則有兩個cache
</td>
</td> </tr>#root.caches[0].name
argument name
</td>執行上下文
</td>當前被調用的方法的參數,如findById(Long id),我們可以通過#id拿到參數
</td>#user.id
</td> </tr>result
</td>執行上下文
</td>方法執行后的返回值(僅當方法執行之后的判斷有效,如‘unless’,'cache evict'的beforeInvocation=false)
</td>
</td> </tr> </tbody> </table>#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生成器,然后通過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)
有時候我們可能組合多個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)
問題及解決方案
一、比如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進行緩存抽象。
歡迎加入spring群134755960進行交流。
示例請參考github:
https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-cache
來自:http://my.oschina.net/u/1435252/blog/191368本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關資訊
sesese色