Spring Cache使用
記錄下自己項目在用的Spring Cache的使用方式。
Spring的抽象已經做得夠好了,適合于大多數場景,非常復雜的就需要自己AOP實現了。
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):最近最少使用算法,一定時間段內使用次數(頻率)最少的那個被移除;
</ul>TTL(Time To Live )
存活期,即從緩存中創建時間點開始直到它到期的一個時間段(不管在這個時間段內有沒有訪問都將過期)
TTI(Time To Idle)
空閑期,即一個數據多久沒被訪問將從緩存中移除的時間。
到此,基本了解了緩存的知識,在Java中,我們一般對調用方法進行緩存控制,比如我調用”findUserById(Long id)”,那么我應該在調用這個方法之前先從緩存中查找有沒有,如果沒有再掉該方法如從數據庫加載用戶,然后添加到緩存中,下次調用時將會從緩存中獲取到數據。
自Spring 3.1起,提供了類似于@Transactional注解事務的注解Cache支持,且提供了Cache抽象;在此之前一般通過AOP實現;使用Spring Cache的好處:
- 提供基本的Cache抽象,方便切換各種底層Cache;
- 通過注解Cache可以實現類似于事務一樣,緩存邏輯透明的應用到我們的業務代碼上,且只需要更少的代碼就可以完成;
- 提供事務回滾時也自動回滾緩存;
- 支持比較復雜的緩存邏輯;
</ul>對于Spring Cache抽象,主要從以下幾個方面學習:
- Cache API及默認提供的實現
- Cache注解
- 實現復雜的Cache邏輯
</ul> 緩存簡介開濤的博客 </blockquote>Spring Cache簡介
Spring3.1開始引入了激動人心的基于注釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實現方案(例如EHCache 或者 OSCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,即能夠達到緩存方法的返回對象的效果。
Spring的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如EHCache、 memcached集成。
其特點總結如下:
- 通過少量的配置 annotation 注釋即可使得既有代碼支持緩存
- 支持開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件即可使用緩存
- 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
- 支持 AspectJ,并通過其實現任何方法的緩存支持
- 支持自定義 key 和自定義緩存管理者,具有相當的靈活性和擴展性
</ul> Spring Cache 介紹Spring Cache 介紹 - Rollen Holt - 博客園 </blockquote>API介紹
Cache接口
理解這個接口有助于我們實現自己的緩存管理器
package org.springframework.cache;public interface Cache {
/** * 緩存的名字 */ String getName(); /** * 得到底層使用的緩存 */ Object getNativeCache(); /** * 根據key得到一個ValueWrapper,然后調用其get方法獲取值 */ ValueWrapper get(Object key); /** * 根據key,和value的類型直接獲取value */ <T> T get(Object key, Class<T> type); /** * 存數據 */ void put(Object key, Object value); /** * 如果值不存在,則添加,用來替代如下代碼 * Object existingValue = cache.get(key); * if (existingValue == null) { * cache.put(key, value); * return null; * } else { * return existingValue; * } */ ValueWrapper putIfAbsent(Object key, Object value); /** * 根據key刪數據 */ void evict(Object key); /** * 清空數據 */ void clear(); /** * 緩存值的Wrapper */ interface ValueWrapper { /** * 得到value */ Object get(); }
}</pre>
默認實現
默認已經實現了幾個常用的cache
位于spring-context-x.RELEASE.jar和spring-context-support-x.RELEASE.jar的cache目錄下
- ConcurrentMapCache:基于java.util.concurrent.ConcurrentHashMap
- GuavaCache:基于Google的Guava工具
- EhCacheCache:基于Ehcache
- JCacheCache:基于javax.cache.Cache(不常用)
</ul>CacheManager
用來管理多個cache
package org.springframework.cache;import java.util.Collection;
public interface CacheManager {
/** * 根據cache名獲取cache */ Cache getCache(String name); /** * 得到所有cache的名字 */ Collection<String> getCacheNames();
}</pre>
默認實現
對應Cache接口的默認實現
- ConcurrentMapCacheManager / ConcurrentMapCacheFactoryBean
- GuavaCacheManager
- EhCacheCacheManager / EhCacheManagerFactoryBean
- JCacheCacheManager / JCacheManagerFactoryBean
</ul>CompositeCacheManager
用于組合CacheManager,可以從多個CacheManager中輪詢得到相應的Cache
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"> <property name="cacheManagers"> <list> <ref bean="concurrentMapCacheManager"/> <ref bean="guavaCacheManager"/> </list> </property> <!-- 都找不到時,不返回null,而是返回NOP的Cache --> <property name="fallbackToNoOpCache" value="true"/> </bean>
事務
除GuavaCacheManager外,其他Cache都支持Spring事務,如果注解方法出現事務回滾,對應緩存操作也會回滾
緩存策略
都是Cache自行維護,Spring只提供對外抽象API
Cache注解
每個注解都有多個參數,這里不一一列出,建議進入源碼查看注釋
啟用注解
</tr> </tbody> </table> <h3>@CachePut </h3> <cache:annotation-driven cache-manager="cacheManager"/>
寫數據
@CachePut(value = "addPotentialNoticeCache", key = "targetClass + '.' + #userCode") public List<PublicAutoAddPotentialJob.AutoAddPotentialNotice> put(int userCode, List<PublicAutoAddPotentialJob.AutoAddPotentialNotice> noticeList) { LOGGER.info("緩存({})的公客自動添加潛在客的通知", userCode); return noticeList; }
<h3>@CacheEvict </h3>失效數據
@CacheEvict(value = "addPotentialNoticeCache", key = "targetClass + '.' + #userCode") public void remove(int userCode) { LOGGER.info("清除({})的公客自動添加潛在客的通知", userCode); }
<h3>@Cacheable </h3>這個用的比較多
用在查詢方法上,先從緩存中讀取,如果沒有再調用方法獲取數據,然后把數據添加到緩存中@Cacheable(value = "kyAreaCache", key="targetClass + '.' + methodName + '.' + #areaId") public KyArea findById(String areaId) { // 業務代碼省略 }
運行流程
- 首先執行@CacheEvict(如果beforeInvocation=true且condition 通過),如果allEntries=true,則清空所有
- 接著收集@Cacheable(如果condition 通過,且key對應的數據不在緩存),放入cachePutRequests(也就是說如果cachePutRequests為空,則數據在緩存中)
- 如果cachePutRequests為空且沒有@CachePut操作,那么將查找@Cacheable的緩存,否則result=緩存數據(也就是說只要當沒有cache put請求時才會查找緩存)
- 如果沒有找到緩存,那么調用實際的API,把結果放入result
- 如果有@CachePut操作(如果condition 通過),那么放入cachePutRequests
- 執行cachePutRequests,將數據寫入緩存(unless為空或者unless解析結果為false);
- 執行@CacheEvict(如果beforeInvocation=false 且 condition 通過),如果allEntries=true,則清空所有
</ol>SpEL上下文數據
在使用時,#root.methodName 等同于 methodName
名稱 位置 描述 示例 </tr> </tbody>methodName root對象 當前被調用的方法名 #root.methodName </tr>method root對象 當前被調用的方法 #root.method.name </tr>target root對象 當前被調用的目標對象 #root.target </tr>targetClass root對象 當前被調用的目標對象類 #root.targetClass </tr>args root對象 當前被調用的方法的參數列表 #root.args[0] </tr>caches root對象 當前方法調用使用的緩存列表(如@Cacheable(value={“cache1”, “cache2”})),則有兩個cache #root.caches[0].name </tr>argument name 執行上下文 當前被調用的方法的參數,如findById(Long id),我們可以通過#id拿到參數 #user.id </tr>result 執行上下文 方法執行后的返回值(僅當方法執行之后的判斷有效,如‘unless’,’cache evict’的beforeInvocation=false) #result </tr> </tbody> </table>條件緩存
主要是在注解內用condition和unless的表達式分別對參數和返回結果進行篩選后緩存
<h3>@Caching </h3>多個緩存注解組合使用
@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) {}</pre>
自定義緩存注解
把一些特殊場景的注解包裝到一個獨立的注解中,比如@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) {
}</pre>
示例
基于ConcurrentMapCache
自定義CacheManager
我需要使用有容量限制和緩存失效時間策略的Cache,默認的ConcurrentMapCacheManager沒法滿足
通過實現CacheManager接口定制出自己的CacheManager。
還是拷貝ConcurrentMapCacheManager,使用Guava的Cache做底層容器,因為Guava的Cache容器可以設置緩存策略新增了exp、maximumSize兩個策略變量
修改底層Cache容器的創建下面只列出自定義的代碼,其他的都是Spring的ConcurrentMapCacheManager的代碼
import com.google.common.cache.CacheBuilder; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.concurrent.ConcurrentMapCache;import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
/**
- 功能說明:自定義的ConcurrentMapCacheManager,新增超時時間和最大存儲限制
作者:liuxing(2015-04-13 18:44) */ public class ConcurrentMapCacheManager implements CacheManager {
/**
- 過期時間,秒(自定義) */ private long exp = 1800; /**
最大存儲數量 (自定義) */ private long maximumSize = 1000;
public void setExp(long exp) { this.exp = exp; }
public void setMaximumSize(long maximumSize) { this.maximumSize = maximumSize; }
/**
- 創建一個緩存容器,這個方法改寫為使用Guava的Cache
- @param name
- @return */ protected Cache createConcurrentMapCache(String name) { return new ConcurrentMapCache(name, CacheBuilder.newBuilder().expireAfterWrite(this.exp, TimeUnit.SECONDS)
} }</pre>.maximumSize(this.maximumSize) .build() .asMap(), isAllowNullValues());
初始化
xml風格
<!-- 啟用緩存注解功能,這個是必須的,否則注解不會生效,指定一個默認的Manager,否則需要在注解使用時指定Manager --> <cache:annotation-driven cache-manager="memoryCacheManager"/><!-- 本地內存緩存 --> <bean id="memoryCacheManager" class="com.dooioo.ky.cache.ConcurrentMapCacheManager" p:maximumSize="2000" p:exp="1800"> <property name="cacheNames"> <list> <value>kyMemoryCache</value> </list> </property> </bean></pre>
使用
@Cacheable(value = "kyMemoryCache", key="targetClass + '.' + methodName") public Map<String, String> queryMobiles(){ // 業務代碼省略 }
使用Memcached
一般常用的緩存當屬memcached了,這個就需要自己實現CacheManager和Cache
注意我實現的Cache里面有做一些定制化操作,比如對key的處理創建MemcachedCache
import com.dooioo.common.jstl.DyFunctions; import com.dooioo.commons.Strings; import com.google.common.base.Joiner; import net.rubyeye.xmemcached.MemcachedClient; import net.rubyeye.xmemcached.exception.MemcachedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper;import java.util.concurrent.TimeoutException;
/**
- 功能說明:自定義spring的cache的實現,參考cache包實現
作者:liuxing(2015-04-12 13:57) */ public class MemcachedCache implements Cache {
private static final Logger LOGGER = LoggerFactory.getLogger(MemcachedCache.class);
/**
- 緩存的別名 */ private String name; /**
- memcached客戶端 */ private MemcachedClient client; /**
- 緩存過期時間,默認是1小時
- 自定義的屬性 */ private int exp = 3600; /**
- 是否對key進行base64加密 */ private boolean base64Key = false; /**
前綴名 */ private String prefix;
@Override public String getName() { return name; }
@Override public Object getNativeCache() { return this.client; }
@Override public ValueWrapper get(Object key) { Object object = null; try {
object = this.client.get(handleKey(objectToString(key)));
} catch (TimeoutException e) {
LOGGER.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} catch (MemcachedException e) {
LOGGER.error(e.getMessage(), e);
}
return (object != null ? new SimpleValueWrapper(object) : null); }
@Override public <T> T get(Object key, Class<T> type) { try {
Object object = this.client.get(handleKey(objectToString(key))); return (T) object;
} catch (TimeoutException e) {
LOGGER.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} catch (MemcachedException e) {
LOGGER.error(e.getMessage(), e);
}
return null; }
@Override public void put(Object key, Object value) { if (value == null) { // this.evict(key);
return;
}
try {
this.client.set(handleKey(objectToString(key)), exp, value);
} catch (TimeoutException e) {
LOGGER.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} catch (MemcachedException e) {
LOGGER.error(e.getMessage(), e);
} }
@Override public ValueWrapper putIfAbsent(Object key, Object value) { this.put(key, value); return this.get(key); }
@Override public void evict(Object key) { try {
this.client.delete(handleKey(objectToString(key)));
} catch (TimeoutException e) {
LOGGER.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} catch (MemcachedException e) {
LOGGER.error(e.getMessage(), e);
} }
@Override public void clear() { try {
this.client.flushAll();
} catch (TimeoutException e) {
LOGGER.error(e.getMessage(), e);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} catch (MemcachedException e) {
LOGGER.error(e.getMessage(), e);
} }
public void setName(String name) { this.name = name; }
public MemcachedClient getClient() { return client; }
public void setClient(MemcachedClient client) { this.client = client; }
public void setExp(int exp) { this.exp = exp; }
public void setBase64Key(boolean base64Key) { this.base64Key = base64Key; }
public void setPrefix(String prefix) { this.prefix = prefix; }
/**
- 處理key
- @param key
@return */ private String handleKey(final String key) { if (base64Key) {
return Joiner.on(EMPTY_SEPARATOR).skipNulls().join(this.prefix, DyFunctions.base64Encode(key));
}
return Joiner.on(EMPTY_SEPARATOR).skipNulls().join(this.prefix, key); }
/**
- 轉換key,去掉空格
- @param object
@return */ private String objectToString(Object object) { if (object == null) {
return null;
} else if (object instanceof String) {
return Strings.replace((String) object, " ", "_");
} else {
return object.toString();
} }
private static final String EMPTY_SEPARATOR = "";
}</pre>
創建MemcachedCacheManager
繼承AbstractCacheManager
import org.springframework.cache.Cache; import org.springframework.cache.support.AbstractCacheManager;import java.util.Collection;
/**
- 功能說明:memcachedCacheManager
作者:liuxing(2015-04-12 15:13) */ public class MemcachedCacheManager extends AbstractCacheManager {
private Collection<Cache> caches;
@Override protected Collection<? extends Cache> loadCaches() {
return this.caches;
}
public void setCaches(Collection<Cache> caches) {
this.caches = caches;
}
public Cache getCache(String name) {
return super.getCache(name);
}
}</pre>
初始化
<!-- 啟用緩存注解功能,這個是必須的,否則注解不會生效,指定一個默認的Manager,否則需要在注解使用時指定Manager --> <cache:annotation-driven cache-manager="cacheManager"/><!-- memcached緩存管理器 --> <bean id="cacheManager" class="com.dooioo.ky.cache.MemcachedCacheManager"> <property name="caches"> <set> <bean class="com.dooioo.ky.cache.MemcachedCache" p:client-ref="ky.memcachedClient" p:name="kyAreaCache" p:exp="86400"/> <bean class="com.dooioo.ky.cache.MemcachedCache" p:client-ref="ky.memcachedClient" p:name="kyOrganizationCache" p:exp="3600"/> </set> </property> </bean></pre>
使用
@Cacheable(value = "kyAreaCache", key="targetClass + '.' + methodName + '.' + #areaId") public KyArea findById(String areaId) { // 業務代碼省略 }
更多
更多復雜的使用場景和注解語法請自行谷歌!
參考
http://docs.spring.io/spring/docs/4.1.x/spring-framework-reference/html/cache.htmlhttp://www.cnblogs.com/rollenholt/p/4202631.html
http://jinnianshilongnian.iteye.com/blog/2001040
來自:http://liuxing.info/2015/06/18/Spring%20Cache%E4%BD%BF%E7%94%A8/
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關經驗
相關資訊
sesese色