1. 程式人生 > >google Guava Cache使用--向本地快取的,輕量級的Cache,適合快取少量資料

google Guava Cache使用--向本地快取的,輕量級的Cache,適合快取少量資料

前言

專案中需要按照時間維度定期清理map中的資料,清理資料時還需要有個回撥能夠做點其他事情,此場景使用Guava Cache非常合適,因此對Guava Cache做個總結。在多執行緒高併發場景中往往是離不開cache的,需要根據不同的應用場景來需要選擇不同的cache,比如分散式快取如Redis、memcached,還有本地(程序內)快取如ehcache、GuavaCache。之前用spring cache的時候整合的是ehcache,但接觸到GuavaCache之後,被它的簡單、強大、及輕量級所吸引。它不需要配置檔案,使用起來和ConcurrentHashMap一樣簡單,而且能覆蓋絕大多數使用cache的場景需求!

GuavaCache是google開源Java類庫Guava的其中一個模組,在maven工程下使用可在pom檔案加入如下依賴:

<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>19.0</version>  
</dependency> 

Cache介面及其實現

先說說一般的cache都會實現的基礎功能包括:

提供一個儲存快取的容器,該容器實現了存放(Put)和讀取(Get)快取的介面供外部呼叫。 快取通常以<key,value>的形式存在,通過key來從快取中獲取value。當然容器的大小往往是有限的(受限於記憶體大小),需要為它設定清除快取的策略。

在GuavaCache中快取的容器被定義為介面Cache<K, V>的實現類,這些實現類都是執行緒安全的,因此通常定義為一個單例。並且介面Cache是泛型,很好的支援了不同型別的key和value。作為示例,我們構建一個key為Integer、value為String的Cache例項:

final static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
        //設定cache的初始大小為10,要合理設定該值  
        .initialCapacity(10)  
        //設定併發數為5,即同一時間最多隻能有5個執行緒往cache執行寫入操作  
        .concurrencyLevel(5)  
        //設定cache中的資料在寫入之後的存活時間為10秒  
        .expireAfterWrite(10, TimeUnit.SECONDS)  
        //構建cache例項  
        .build();  

據說GuavaCache的實現是基於ConcurrentHashMap的,因此上面的構造過程所呼叫的方法,通過檢視其官方文件也能看到一些類似的原理。比如通過initialCapacity(5)定義初始值大小,要是定義太大就好浪費記憶體空間,要是太小,需要擴容的時候就會像map一樣需要resize,這個過程會產生大量需要gc的物件,還有比如通過concurrencyLevel(5)來限制寫入操作的併發數,這和ConcurrentHashMap的鎖機制也是類似的(ConcurrentHashMap讀不需要加鎖,寫入需要加鎖,每個segment都有一個鎖)。

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

/** 
 * 該介面的實現被認為是執行緒安全的,即可在多執行緒中呼叫 
 * 通過被定義單例使用 
 */  
public interface Cache<K, V> {  
  
  /** 
   * 通過key獲取快取中的value,若不存在直接返回null 
   */  
  V getIfPresent(Object key);  
  
  /** 
   * 通過key獲取快取中的value,若不存在就通過valueLoader來載入該value 
   * 整個過程為 "if cached, return; otherwise create, cache and return" 
   * 注意valueLoader要麼返回非null值,要麼丟擲異常,絕對不能返回null 
   */  
  V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;  
  
  /** 
   * 新增快取,若key存在,就覆蓋舊值 
   */  
  void put(K key, V value);  
  
  /** 
   * 刪除該key關聯的快取 
   */  
  void invalidate(Object key);  
  
  /** 
   * 刪除所有快取 
   */  
  void invalidateAll();  
  
  /** 
   * 執行一些維護操作,包括清理快取 
   */  
  void cleanUp();  
} 

使用過程還是要認真檢視官方的文件,以下Demo簡單的展示了Cache的寫入,讀取,和過期清除策略是否生效:

public static void main(String[] args) throws Exception {  
    cache.put(1, "Hi");  
      
    for(int i=0 ;i<100 ;i++) {  
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
        System.out.println(sdf.format(new Date())   
                + "  key:1 ,value:"+cache.getIfPresent(1));  
        Thread.sleep(1000);  
    }  
}  

下面是get的使用示例,注意get不可返回null,假如要返回null用catch 捕捉住異常就好

public static Object get(Object key) throws ExecutionException {


Object var = cache.get(key, new Callable<Object>() {

@Override

public Object call() throws Exception {

System.out.println("如果沒有值,就執行其他方式去獲取值");

String var = "Google.com.sg";

return var;

}

});

return var;

}


public static void put(Object key, Object value) {

cache.put(key, value);

}


返回null的get

public static Object get(Object key) {

try{

Object var = cache.get(key, new Callable<Object>() {

@Override

public Object call() throws Exception {

System.out.println("如果沒有值,就執行其他方式去獲取值");


return null;

}

});

return var;

} catch(Exception e) {

return null;

}

}

清除快取的策略

任何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中,並不存在任何執行緒!它實現機制是在寫操作時順帶做少量的維護工作(如清除),偶爾在讀操作時做(如果寫操作實在太少的話),也就是說在使用的是呼叫執行緒,參考如下示例:

public class CacheService {  
    static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
            .expireAfterWrite(5, TimeUnit.SECONDS)  
            .build();  
      
    public static void main(String[] args) throws Exception {  
        new Thread() { //monitor  
            public void run() {  
                while(true) {  
                    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
                    System.out.println(sdf.format(new Date()) +" size: "+cache.size());  
                    try {  
                        Thread.sleep(2000);  
                    } catch (InterruptedException e) {  
                    }  
                }  
            };  
        }.start();  
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
        cache.put(1, "Hi");  
        System.out.println("write key:1 ,value:"+cache.getIfPresent(1));  
        Thread.sleep(10000);  
        // when write ,key:1 clear  
        cache.put(2, "bbb");  
        System.out.println("write key:2 ,value:"+cache.getIfPresent(2));  
        Thread.sleep(10000);  
        // when read other key ,key:2 do not clear  
        System.out.println(sdf.format(new Date())  
                +" after write, key:1 ,value:"+cache.getIfPresent(1));  
        Thread.sleep(2000);  
        // when read same key ,key:2 clear  
        System.out.println(sdf.format(new Date())  
                +" final, key:2 ,value:"+cache.getIfPresent(2));  
    }  
}  

控制檯輸出:

00:34:17 size: 0  
write key:1 ,value:Hi  
00:34:19 size: 1  
00:34:21 size: 1  
00:34:23 size: 1  
00:34:25 size: 1  
write key:2 ,value:bbb  
00:34:27 size: 1  
00:34:29 size: 1  
00:34:31 size: 1  
00:34:33 size: 1  
00:34:35 size: 1  
00:34:37 after write, key:1 ,value:null  
00:34:37 size: 1  
00:34:39 final, key:2 ,value:null  
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。