GuavaCache簡介(一)

原文地址 http://blog.csdn.net/guozebo/article/details/51590517

前言

在多線程高併發場景中每每是離不開cache的,須要根據不一樣的應用場景來須要選擇不一樣的cache,好比分佈式緩存如redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCache。以前用spring cache的時候集成的是ehcache,但接觸到GuavaCache以後,被它的簡單、強大、及輕量級所吸引。它不須要配置文件,使用起來和ConcurrentHashMap同樣簡單,並且能覆蓋絕大多數使用cache的場景需求!html

GuavaCache是google開源java類庫Guava的其中一個模塊,在maven工程下使用可在pom文件加入以下依賴:java

 

[html]  view plain  copy
 
  1. <dependency>  
  2.     <groupId>com.google.guava</groupId>  
  3.     <artifactId>guava</artifactId>  
  4.     <version>19.0</version>  
  5. </dependency>  

 

 

Cache接口及其實現

先說說通常的cache都會實現的基礎功能包括:git

提供一個存儲緩存的容器,該容器實現了存放(Put)和讀取(Get)緩存的接口供外部調用。 緩存一般以<key,value>的形式存在,經過key來從緩存中獲取value。固然容器的大小每每是有限的(受限於內存大小),須要爲它設置清除緩存的策略。github

在GuavaCache中緩存的容器被定義爲接口Cache<K, V>的實現類,這些實現類都是線程安全的,所以一般定義爲一個單例。而且接口Cache是泛型,很好的支持了不一樣類型的key和value。做爲示例,咱們構建一個key爲Integer、value爲String的Cache實例:redis

 

[java]  view plain  copy
 
  1. final static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
  2.         //設置cache的初始大小爲10,要合理設置該值  
  3.         .initialCapacity(10)  
  4.         //設置併發數爲5,即同一時間最多隻能有5個線程往cache執行寫入操做  
  5.         .concurrencyLevel(5)  
  6.         //設置cache中的數據在寫入以後的存活時間爲10秒  
  7.         .expireAfterWrite(10, TimeUnit.SECONDS)  
  8.         //構建cache實例  
  9.         .build();  

聽說GuavaCache的實現是基於ConcurrentHashMap的,所以上面的構造過程所調用的方法,經過查看其官方文檔也能看到一些相似的原理。好比經過initialCapacity(5)定義初始值大小,要是定義太大就好浪費內存空間,要是過小,須要擴容的時候就會像map同樣須要resize,這個過程會產生大量須要gc的對象,還有好比經過concurrencyLevel(5)來限制寫入操做的併發數,這和ConcurrentHashMap的鎖機制也是相似的(ConcurrentHashMap讀不須要加鎖,寫入須要加鎖,每一個segment都有一個鎖)。spring

 

接下來看看Cache提供哪些方法(只列了部分經常使用的):緩存

 

[java]  view plain  copy
 
  1. /** 
  2.  * 該接口的實現被認爲是線程安全的,便可在多線程中調用 
  3.  * 經過被定義單例使用 
  4.  */  
  5. public interface Cache<K, V> {  
  6.   
  7.   /** 
  8.    * 經過key獲取緩存中的value,若不存在直接返回null 
  9.    */  
  10.   V getIfPresent(Object key);  
  11.   
  12.   /** 
  13.    * 經過key獲取緩存中的value,若不存在就經過valueLoader來加載該value 
  14.    * 整個過程爲 "if cached, return; otherwise create, cache and return" 
  15.    * 注意valueLoader要麼返回非null值,要麼拋出異常,絕對不能返回null 
  16.    */  
  17.   V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;  
  18.   
  19.   /** 
  20.    * 添加緩存,若key存在,就覆蓋舊值 
  21.    */  
  22.   void put(K key, V value);  
  23.   
  24.   /** 
  25.    * 刪除該key關聯的緩存 
  26.    */  
  27.   void invalidate(Object key);  
  28.   
  29.   /** 
  30.    * 刪除全部緩存 
  31.    */  
  32.   void invalidateAll();  
  33.   
  34.   /** 
  35.    * 執行一些維護操做,包括清理緩存 
  36.    */  
  37.   void cleanUp();  
  38. }  

使用過程仍是要認真查看官方的文檔,如下Demo簡單的展現了Cache的寫入,讀取,和過時清除策略是否生效:安全

 

 

[java]  view plain  copy
 
  1. public static void main(String[] args) throws Exception {  
  2.     cache.put(1, "Hi");  
  3.       
  4.     for(int i=0 ;i<100 ;i++) {  
  5.         SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  6.         System.out.println(sdf.format(new Date())   
  7.                 + "  key:1 ,value:"+cache.getIfPresent(1));  
  8.         Thread.sleep(1000);  
  9.     }  
  10. }  

 

清除緩存的策略

任何Cache的容量都是有限的,而緩存清除策略就是決定數據在何時應該被清理掉。GuavaCache提了如下幾種清除策略:
 

基於存活時間的清除(Timed Eviction)

這應該是最經常使用的清除策略,在構建Cache實例的時候,CacheBuilder提供兩種基於存活時間的構建方法:
(1)expireAfterAccess(long, TimeUnit):緩存項在建立後,在給定時間內沒有被讀/寫訪問,則清除。
(2)expireAfterWrite(long, TimeUnit):緩存項在建立後,在給定時間內沒有被寫訪問(建立或覆蓋),則清除。
expireAfterWrite()方法有些相似於redis中的expire命令,但顯然它只能設置全部緩存都具備相同的存活時間。若遇到一些緩存數據的存活時間爲1分鐘,一些爲5分鐘,那隻能構建兩個Cache實例了。
 

基於容量的清除(size-based eviction)

在構建Cache實例的時候,經過CacheBuilder.maximumSize(long)方法能夠設置Cache的最大容量數,當緩存數量達到或接近該最大值時,Cache將清除掉那些最近最少使用的緩存。
以上是這種方式是以緩存的「數量」做爲容量的計算方式,還有另一種基於「權重」的計算方式。好比每一項緩存所佔據的內存空間大小都不同,能夠看做它們有不一樣的「權重」(weights)。你可使用CacheBuilder.weigher(Weigher)指定一個權重函數,而且用CacheBuilder.maximumWeight(long)指定最大總重。
 

顯式清除

任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收,Cache接口提供了以下API:
(1)個別清除:Cache.invalidate(key)
(2)批量清除:Cache.invalidateAll(keys)
(3)清除全部緩存項:Cache.invalidateAll()

 

 

基於引用的清除(Reference-based Eviction)

在構建Cache實例過程當中,經過設置使用弱引用的鍵、或弱引用的值、或軟引用的值,從而使JVM在GC時順帶實現緩存的清除,不過通常不輕易使用這個特性。
(1)CacheBuilder.weakKeys():使用弱引用存儲鍵
(2)CacheBuilder.weakValues():使用弱引用存儲值
(3)CacheBuilder.softValues():使用軟引用存儲值
 

清除何時發生?

也許這個問題有點奇怪,若是設置的存活時間爲一分鐘,難道不是一分鐘後這個key就會當即清除掉嗎?咱們來分析一下若是要實現這個功能,那Cache中就必須存在線程來進行週期性地檢查、清除等工做,不少cache如redis、ehcache都是這樣實現的。
但在GuavaCache中,並不存在任何線程!它實現機制是在寫操做時順帶作少許的維護工做(如清除),偶爾在讀操做時作(若是寫操做實在太少的話),也就是說在使用的是調用線程,參考以下示例:
[java]  view plain  copy
 
  1. public class CacheService {  
  2.     static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
  3.             .expireAfterWrite(5, TimeUnit.SECONDS)  
  4.             .build();  
  5.       
  6.     public static void main(String[] args) throws Exception {  
  7.         new Thread() { //monitor  
  8.             public void run() {  
  9.                 while(true) {  
  10.                     SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  11.                     System.out.println(sdf.format(new Date()) +" size: "+cache.size());  
  12.                     try {  
  13.                         Thread.sleep(2000);  
  14.                     } catch (InterruptedException e) {  
  15.                     }  
  16.                 }  
  17.             };  
  18.         }.start();  
  19.         SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  20.         cache.put(1, "Hi");  
  21.         System.out.println("write key:1 ,value:"+cache.getIfPresent(1));  
  22.         Thread.sleep(10000);  
  23.         // when write ,key:1 clear  
  24.         cache.put(2, "bbb");  
  25.         System.out.println("write key:2 ,value:"+cache.getIfPresent(2));  
  26.         Thread.sleep(10000);  
  27.         // when read other key ,key:2 do not clear  
  28.         System.out.println(sdf.format(new Date())  
  29.                 +" after write, key:1 ,value:"+cache.getIfPresent(1));  
  30.         Thread.sleep(2000);  
  31.         // when read same key ,key:2 clear  
  32.         System.out.println(sdf.format(new Date())  
  33.                 +" final, key:2 ,value:"+cache.getIfPresent(2));  
  34.     }  
  35. }  
控制檯輸出:
[java]  view plain  copy
 
  1. 00:34:17 size: 0  
  2. write key:1 ,value:Hi  
  3. 00:34:19 size: 1  
  4. 00:34:21 size: 1  
  5. 00:34:23 size: 1  
  6. 00:34:25 size: 1  
  7. write key:2 ,value:bbb  
  8. 00:34:27 size: 1  
  9. 00:34:29 size: 1  
  10. 00:34:31 size: 1  
  11. 00:34:33 size: 1  
  12. 00:34:35 size: 1  
  13. 00:34:37 after write, key:1 ,value:null  
  14. 00:34:37 size: 1  
  15. 00:34:39 final, key:2 ,value:null  
  16. 00:34:39 size: 0  
經過分析發現:
(1)緩存項<1,"Hi">的存活時間是5秒,但通過5秒後並無被清除,由於仍是size=1
(2)發生寫操做cache.put(2, "bbb")後,緩存項<1,"Hi">被清除,由於size=1,而不是size=2
(3)發生讀操做cache.getIfPresent(1)後,緩存項<2,"bbb">沒有被清除,由於仍是size=1,看來讀操做確實不必定會發生清除
(4)發生讀操做cache.getIfPresent(2)後,緩存項<2,"bbb">被清除,由於讀的key就是2

這在GuavaCache被稱爲「延遲刪除」,即刪除老是發生得比較「晚」,這也是GuavaCache不一樣於其餘Cache的地方!這種實現方式的問題:緩存會可能會存活比較長的時間,一直佔用着內存。若是使用了複雜的清除策略如 基於容量的清除,還可能會佔用着線程而致使響應時間變長。但優勢也是顯而易見的,沒有啓動線程,不論是實現,仍是使用起來都讓人以爲簡單(輕量)。
若是你仍是但願儘量的下降延遲,能夠建立本身的維護線程,以固定的時間間隔調用Cache.cleanUp(),ScheduledExecutorService能夠幫助你很好地實現這樣的定時調度。不過這種方式依然沒辦法百分百的肯定必定是本身的維護線程「命中」了維護的工做。
 
 

總結

請必定要記住GuavaCache的實現代碼中沒有啓動任何線程!!Cache中的全部維護操做,包括清除緩存、寫入緩存等,都是經過調用線程來操做的。這在須要低延遲服務場景中使用時尤爲須要關注,可能會在某個調用的響應時間忽然變大。
GuavaCache畢竟是一款面向本地緩存的,輕量級的Cache,適合緩存少許數據。若是你想緩存上千萬數據,能夠爲每一個key設置不一樣的存活時間,而且高性能,那並不適合使用GuavaCache。



參考

官方github多線程


官方文檔中文翻譯併發

相關文章
相關標籤/搜索