當項目中須要使用local cache的時候,通常都會經過基於 ConcurrentHashMap或者LinkedHashMap來實現本身的LRU Cache。在造輪子過程當中,通常都須要解決一下問題:
1. 內存是有限了,因此須要限定緩存的最大容量.
2. 如何清除「太舊」的緩存entry.
3. 如何應對併發讀寫.
4.緩存數據透明化:命中率、失效率等.
cache的優劣基本取決於如何優雅高效地解決上面這些問題。Guava cache很好地解決了這些問題,是一個很是好的本地緩存,線程安全,功能齊全,簡單易用,性能好。總體上來講Guava cache 是本地緩存的不二之選。
下面是一個簡單地例子:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER) .build(new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key); } });複製代碼
接下來,咱們從多個角度來介紹如何使用Guava cache。
1、建立cache:
通常來講,在工做中咱們通常這樣使用remote cache或者local cache:
User user = cache.get(usernick); if(user == null){ user = userDao.getUser(usernick); cache.put(usernick, user); } return user;
即if cached, return; otherwise create/load/compute, cache and return。
而Guava cache 經過下面兩種方式以一種更優雅的方式實現了這個邏輯:
1. From A CacheLoader
2. From A Callable
經過這兩種方法建立的cache,和普通的用map緩存相比,不一樣在於,都實現了上面提到的——「if cached, return; otherwise create/load/compute, cache and return」。但不一樣的在於cacheloader的定義比較寬泛,是針對整個cache定義的,能夠認爲是統一的根據key值load value的方法。而callable的方式較爲靈活,容許你在get的時候指定。舉兩個栗子來介紹如何使用這兩種方式
From CacheLoader:
LoadingCache<String, String> graphs = CacheBuilder.newBuilder().maximumSize(1000)
.build(new CacheLoader<String, String>() {
public String load(String key) {
// 這裏是key根據實際去取值的方法,例如根據這個key去數據庫或者經過複雜耗時的計算得出
System.out.println("no cache,load from db"); return "123"; } }); String val1 = graphs.get("key"); System.out.println("1 value is: " + val1); String val2 = graphs.get("key"); System.out.println("2 value is: " + val2);From Callable: Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000).build(); String val1 = cache.get("key", new Callable<String>() {
public String call() {
// 這裏是key根據實際去取值的方法,例如根據這個key去數據庫或者經過複雜耗時的計算得出
System.out.println("val call method is invoked"); return "123"; } }); System.out.println("1 value is: " + val1); String val2 = cache.get("testKey", new Callable<String>() {
public String call() {
// 這裏是key根據實際去取值的方法,例如根據這個key去數據庫或者經過複雜耗時的計算得出
System.out.println("val call method is invoked"); return "123"; } }); System.out.println("2 value is: " + val2);複製代碼
須要注意的是,全部的Guava caches,不管是否是loader模式,都支持get(Key,Callable<V>)方法。
另外,除了上述這兩種方式來更新緩存外,Guava cache固然也支持Inserted Directly:Values也能夠經過cache.put(key,value)直接將值插入到cache中。該方法將覆蓋key對應的entry。
2、緩存移除
內存是有限,因此不能把全部的東西都加載到內存中,過大的local cache對任何java應用來講都是噩夢。所以local cache必須提供不一樣的機制來清除「沒必要要」的緩存entry,平衡內存使用率和命中率。Guava Cache提供了3中緩存清除策略:size-based eviction, time-based eviction, and reference-based eviction.
size-based eviction:基於cache容量的移除。若是你的cache不容許擴容,即不容許超過設定的最大值,那麼使用CacheBuilder.maxmuSize(long)便可。在這種條件下,cache會本身釋放掉那些最近沒有或者不常用的entries內存。這裏須要注意一下兩點:
1.並非在超過限定時纔會刪除掉那些entries,而是在即將達到這個限定值時,那麼你就要當心考慮這種狀況了,由於很明顯即便沒有達到這個限定值,cache仍然會進行刪除操做。
2.若是一個key-entry已經被移除了,當你再次調用get(key)時,若是CacheBuilder採用的是CacheLoader模式,那依然會從cacheLoader中加載一次。
此外,若是你的cache裏面的entries有着大相徑庭的內存佔用若是你的cache values有着大相徑庭的內存佔用,你能夠經過CacheBuilder.weigher(Weigher)來爲不一樣的entry設定weigh,而後使用CacheBuilder.maximumWeight(long)設定一個最大值。在tpn會經過local cache緩存用戶對消息類目的訂閱信息,有的用戶訂閱的消息類目比較多,所佔的內存就比較多,有的用戶訂閱的消息類目比較少,天然佔用的內存就比較少。那麼我就能夠經過下面的方法來根據用戶訂閱的消息類目數量設置不一樣的weight,這樣就能夠在不更改cache大小的狀況下,使得緩存儘可能覆蓋更多地用戶:
LoadingCache<Key, User> Users= CacheBuilder.newBuilder()
.maximumWeight(100000) .weigher(new Weigher<Key, User>() {
public int weigh(Key k, User u) {
if(u.categories().szie() >5){
return 2; }else{ return 1; } } }) .build( new CacheLoader<Key, User>() {
public Userload(Key key) { // no checked exception
return createExpensiveUser(key); } });複製代碼
PS:這個例子可能不是很恰當,當時足以說明weight的用法。
time-based eviction:基於時間的移除。Guava cache提供了兩種方法來實現這個邏輯:
1. expireAfterAccess(long, TimeUnit)
從最後一次訪問(讀或者寫)開始計時,過了這段指定的時間就會釋放掉該entries。注意:那些被刪掉的entries的順序時和size-based eviction是十分類似的。
2. expireAfterWrite(long,TimeUnit)
從entries被建立或者最後一次被修改值的點來計時的,若是從這個點開始超過了那段指定的時間,entries就會被刪除掉。這點設計的很精明,由於數據會隨着時間變得愈來愈陳舊。
若是想要測試Timed Eviction,使用Ticker interface和CacheBuilder.ticker(Ticker)方法對你的cache設定一個時間便可,那麼你就不須要去等待系統時間了。
reference-based eviction:基於引用的移除。Guava爲你準備了entries的垃圾回收器,對於keys或者values可使用weak reference ,對於values可使用soft reference.
1. CacheBuilder.weakKeys(): 經過weak reference存儲keys。在這種狀況下,若是keys沒有被strong或者soft引用,那麼entries會被垃圾回收。
2. CacheBuilder.weakValues() : 經過weak referene 存儲values.在這種狀況下,若是valves沒有被strong或者soft引用,那麼entries會被垃圾回收。
須要注意的是:這種條件下的垃圾回收器是創建在引用之上的,那麼這會形成整個cache是使用==來比較倆個key的,而不是equals();
除了上面這三種方式來移除cache的enties外,還能夠經過如下3個方法來主動釋放一些enties:
1. 單獨移除用: Cache.invalidate(key)
2. 批量移除用 :Cache.invalidateAll(keys)
3. 移除全部用 :Cache.invalidateAll()
若是須要在移除數據的時候有所動做還能夠定義Removal Listener,可是有點須要注意的是默認Removal Listener中的行爲是和移除動做同步執行的,若是須要改爲異步形式,能夠考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)。
最後咱們來看一下Guava Cache是何時執行清理動做的。經過CacheBuilder建立的cache既不會自動執行清理和移除entry,也不會在entry過時後立馬執行清除操做。相反,其在執行寫操做或者讀操做的時候(在寫操做很是少的狀況下)來經過少許的操做來執行清理工做。這樣作的緣由是:若是咱們要不斷進行緩存的清理和移除,咱們須要建立一個線程,其業務將與用戶的操做來爭奪共享鎖。此外,某些環境限制清理線程的建立,這將使CacheBuilder沒法使用在該環境中。 所以,Guava cache將什麼時候清理的選擇權交給用戶。若是你的緩存是面向高吞吐量應用的,那麼你沒必要擔憂執行緩存維護,清理過時的entries等。若是你的緩存是面向讀多寫少的應用,爲了不影響緩存讀取,那麼就能夠建立本身的維護線程每隔一段時間就調用Cache.cleanUp(),如能夠用ScheduledExecutorService安排維修。
若是你想安排按期維護緩存的緩存中不多有寫,只是用ScheduledExecutorService安排維修。
3、統計功能:
統計功能是Guava cache一個很是實用的特性。能夠經過CacheBuilder.recordStats() 方法啓動了 cache的數據收集:
1. Cache.stats(): 返回了一個CacheStats對象, 提供一些數據方法
2. hitRate(): 請求點擊率
3. averageLoadPenalty(): 加載new value,花費的時間, 單位nanosecondes
4. evictionCount(): 清除的個數