Guava緩存詳解及使用

緩存

緩存分爲本地緩存與分佈式緩存。本地緩存爲了保證線程安全問題,一般使用ConcurrentMap的方式保存在內存之中,而常見的分佈式緩存則有RedisMongoDB等。

  • 一致性:本地緩存由於數據存儲於內存之中,每個實例都有自己的副本,可能會存在不一致的情況;分佈式緩存則可有效避免這種情況
  • 開銷:本地緩存會佔用JVM內存,會影響GC及系統性能;分佈式緩存的開銷則在於網絡時延和對象序列化,故主要影響調用時延
  • 適用場景:本地緩存適用於數據量較小或變動較少的數據;分佈式緩存則適用於一致性要求較高及數量量大的場景(可彈性擴容)

本地緩存適用於數據量較小或變動較少的數據,因爲變動多需要考慮到不同實例的緩存一致性問題,而數據量大則需要考慮緩存回收策略及GC相關的問題

Guava cache

Guava Cache 是Google Fuava中的一個內存緩存模塊,用於將數據緩存到JVM內存中。

  • 提供了get、put封裝操作,能夠集成數據源 ;
  • 線程安全的緩存,與ConcurrentMap相似,但前者增加了更多的元素失效策略,後者只能顯示的移除元素;
  • Guava Cache提供了多種基本的緩存回收方式
  • 監控緩存加載/命中情況

通常,Guava緩存適用於以下情況:

  • 您願意花費一些內存來提高速度。
  • 您希望有時會多次查詢key。
  • 您的緩存將不需要存儲超出RAM容量的數據

詳細配置

緩存的併發級別

Guava提供了設置併發級別的API,使得緩存支持併發的寫入和讀取。與ConcurrentHashMap類似,Guava cache的併發也是通過分離鎖實現。在通常情況下,推薦將併發級別設置爲服務器cpu核心數。

CacheBuilder.newBuilder()
		// 設置併發級別爲cpu核心數,默認爲4
		.concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
		.build();

緩存的初始容量設置

我們在構建緩存時可以爲緩存設置一個合理大小初始容量,由於Guava的緩存使用了分離鎖的機制,擴容的代價非常昂貴。所以合理的初始容量能夠減少緩存容器的擴容次數。

CacheBuilder.newBuilder()
		// 設置初始容量爲100
		.initialCapacity(100)
		.build();

設置最大存儲

Guava Cache可以在構建緩存對象時指定緩存所能夠存儲的最大記錄數量。當Cache中的記錄數量達到最大值後再調用put方法向其中添加對象,Guava會先從當前緩存的對象記錄中選擇一條刪除掉,騰出空間後再將新的對象存儲到Cache中。

CacheBuilder.newBuilder()
		// 設置最大容量爲1000
		.maximumSize(1000)
		.build();

緩存清除策略

  1. 基於存活時間的清除策略
  • expireAfterWrite 寫緩存後多久過期
  • expireAfterAccess 讀寫緩存後多久過期
    存活時間策略可以單獨設置或組合配置
  1. 基於容量的清除策略
    通過CacheBuilder.maximumSize(long)方法可以設置Cache的最大容量數,當緩存數量達到或接近該最大值時,Cache將清除掉那些最近最少使用的緩存

  2. 基於權重的清除 策略
    使用CacheBuilder.weigher(Weigher)指定一個權重函數,並且用CacheBuilder.maximumWeight(long)指定最大總重。

如每一項緩存所佔據的內存空間大小都不一樣,可以看作它們有不同的“權重”(weights),作爲執行清除策略時優化回收的對象

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumWeight(100000)
       .weigher(new Weigher<Key, Graph>() {
          public int weigh(Key k, Graph g) {
            return g.vertices().size();
          }
        })
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return createExpensiveGraph(key);
             }
           });
  1. 顯式清除
  • 清除單個key:Cache.invalidate(key)
  • 批量清除key:Cache.invalidateAll(keys)
  • 清除所有緩存項:Cache.invalidateAll()
  1. 基於引用的清除策略
    在構建Cache實例過程中,通過設置使用弱引用的鍵、或弱引用的值、或軟引用的值,從而使JVM在GC時順帶實現緩存的清除
  • CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收
  • CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定

垃圾回收僅依賴==恆等式,使用弱引用鍵的緩存用而不是equals(),即同一對象引用。

Cache

顯式put操作置入內存

private static Cache<Integer, Integer> numCache = CacheBuilder.newBuilder()
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();

public static void main(String[] args) throws Exception {
    System.out.println(numCache.getIfPresent(1));
    Thread.sleep(1000);
    System.out.println(numCache.getIfPresent(1));
    Thread.sleep(1000);
    numCache.put(1, 5);
    System.out.println(numCache.getIfPresent(1));
    // console: null null 5
}

LoadingCache

使用自定義ClassLoader加載數據,置入內存中。從LoadingCache中獲取數據時,若數據存在則直接返回;若數據不存在,則根據ClassLoaderload方法加載數據至內存,然後返回該數據

private static LoadingCache<Integer,Integer> numCache = CacheBuilder.newBuilder().
        expireAfterWrite(5L, TimeUnit.MINUTES).
        maximumSize(5000L).
        build(new CacheLoader<Integer, Integer>() {
            @Override
            public Integer load(Integer key) throws Exception {
                System.out.println("no cache");
                return key * 5;
            }
        });

public static void main(String[] args) throws Exception {
    System.out.println(numCache.get(1));
    Thread.sleep(1000);
    System.out.println(numCache.get(1));
    Thread.sleep(1000);
    numCache.put(1, 6);
    System.out.println(numCache.get(1));
    // console: 5 5 6
}

參考資料:

  1. Google CachesExplained
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章