2014年7月19日 星期六

google guava: cached key (time-based)

最近換到了新公司,又開始使用一些從沒有使用過的技術,也有使用一些舊技術…其中的甘苦談就不提了,


 


今天要來分享一個最近使用到的java套件 :google guava: cache



google guava是google分享的一個java套件,


可以有大量的神奇的功能,其中有不少非常實用且容易上手


 


今天要介紹的Cache則是其中之一。


他有點像是ConcureentHashMap ,只是再加上了時間變數


(以下的介紹會以讀者都知道什麼是ConcureentHashMap 來說明)


 


主要的作用就是讓超過時間的物件可以自動從map中移除,因此用Cache稱乎它確實非常洽當。


詳細的Code介紹:


 


最快的寫法:









CacheLoader<String, String> loader = new CacheLoader<String, String>() {
public String load(String key) {
return xxxxx;
}
};


LoadingCache<String, String> cache = CacheBuilder.newBuilder().build(loader);



只需要把xxxxx implement完就可以了。


這一段code,其實就是等於建造了一個ConcurrenctHashMap<String, String>(); 


不同的是你不需要事先把key, value放入。


每一次,當你從cache.get(key) 的時候,當他找不到物件,他會自動的去load去抓取value,並且將資料存入cache裡面去。


 


尤於不是一開始全部輸入,所以其實像是個lazyload 的hashmap


使用上當比一般的hashMap省空間一些。


-----------------------------------------------------------------------------------------------------------------------


但我剛剛說了,這是一個cache, lazyload並不能夠稱為 cache,


因此一般我們會至少加上一個expire的變數。


 


一般會使用 .expireAfterAccess,或是.expireAfterWrite 兩種方法


(P.S. 剛剛看了文件,其實還有refreshAfterAcess, refreshAfterWrite,適用於資料會一直變動的方式, 在這裡先不討論)


expireAfterAccess 是指最後一次Access之後多久會失效 (有點像是LRU Least Recently Used)


expireAfterWrite則是指放入map後多久會失效  (FIFO, first in, first out)


 


最後,還有一個變數:既然是Cache,那麼作戲要作全套,cache 是有空間限制的,因此也可以加上size


.maximumSize(200)


 


因此,如果把整段時間寫出來的話,大概就會變成:


 









CacheLoader<String, String> loader = new CacheLoader<String, String>() {
public String load(String key) {
return xxxxx;
}
};


LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(30)
.expireAfterAccess(2, TimeUnit.MINUTES)
// .expireAfterWrite(2, TimeUnit.MINUTES)
.build(loader);



 


xxxxx還是要自己寫,


expire的時間可以用分鐘,也可以用小時,或是秒,視個人需求而加入。


 


 


本來自己使用的需求到這裡就結束了。


 


但是其實還有一個很常用的removeListener,就一起打出來


Listener主要是在某個key/value要被移除的時候會被觸發


 


(網路上的說明都只有在那邊記錄log,但其實如果這些cache會存到DB的話,也可以在removal的時候將資料存到DB裡面)


先宣告一個RemoveListener









RemovalListener<String, String> listener =
new RemovalListener<String, String>() {
public void onRemoval(
RemovalNotification<String, String> n) {
if (n.wasEvicted()) {
cleanupEntry(n.getKey(), n.getValue());
}
}
};



 


然後將之放進去我們需要的cache裡面即可。


 


所以整段code會再度變成( 其實也只是多了一行)


 









CacheLoader<String, String> loader = new CacheLoader<String, String>() {
public String load(String key) {
return XXXXX;
}
};


 


RemovalListener<String, String> listener =
new RemovalListener<String, String>() {
public void onRemoval(
RemovalNotification<String, String> n) {
if (n.wasEvicted()) {
cleanupEntry(n.getKey(), n.getValue());
}
}
};



LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(200)
.expireAfterAccess(2, TimeUnit.MINUTES)
// .expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(listener)
.build(loader);



 


Guava真的是一個很好用的api,比起我同時在研究的shiro,這個真的簡單易懂多了。


 


更多的功能介紹可以看一下guava cache的介紹投影片:


介紹投影片:


http://guava-libraries.googlecode.com/files/JavaCachingwithGuava.pdf


 


 


要注意的問題:


1: NoSuchMethodException : assertNotNull


   這是一個莫名其妙的bug,後來找了很久,發現是因為多灌了一個google-collection 的jar檔,移除後就沒事了


   至於為什麼,我到現在還是不知道為什麼,個人覺得可能是namespace相衝,使得method名稱重複的問題導致。


   但又覺得google不大可能犯這種錯誤,所以……


2: guava之中非常不愛null,所以盡可能的不要直接回傳null,如果在loader回傳null,他會傳出Exception,而且下一次還是會再來嘗試一次。


我自己的作法是定義一個DEFAULT_VALUE, 如果找不到就回傳該變數,詳情可以參考


https://code.google.com/p/guava-libraries/wiki/UsingAndAvoidingNullExplained


我自己的寫法: ( 以下不是在COMPILER打的,只是隨便打一個範例而已)









Public static final EMPTY_VALUE = "";


private String xxxxx(String key){


    String ret = loadDB(key);


    if (ret==null) 


        return EMPTY_VALUE;


    return ret;


}


 



這樣子然後自己處理資料的時候,只要記得要另外處理"" (空字串)就可以了


而且很多時候空字串就讓它空著沒有關係。會節省很多時間。


 


如果value不是字串,而是自定的class,也可以用類似的方法來處理。


 


 


3: 自己試用的時候,如果有設maximum size的時候,如果該size尚未用完,他不會主動移掉cache.


所以不只是lazyload,而且還是lazyRemove。這點也要特別注意。


 


 


 


 


沒有留言:

張貼留言