guava cache使用和源碼分析


  • guava cache的優點和使用場景,用來判斷業務中是否適合使用此緩存
  • 介紹常用的方法,並給出示例,作為使用的參考
  • 深入解讀源碼。

guava簡介

guava cache是一個本地緩存。有以下優點:

  • 很好的封裝了get、put操作,能夠集成數據源。
    一般我們在業務中操作緩存,都會操作緩存和數據源兩部分。如:put數據時,先插入DB,再刪除原來的緩存;ge數據時,先查緩存,命中則返回,沒有命中時,需要查詢DB,再把查詢結果放入緩存中。 guava cache封裝了這么多步驟,只需要調用一次get/put方法即可。
  • 線程安全的緩存,與ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能顯示的移除元素。
  • Guava Cache提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。定時回收有兩種:按照寫入時間,最早寫入的最先回收;按照訪問時間,最早訪問的最早回收。
  • 監控緩存加載/命中情況。

常用方法

  • V getIfPresent(Object key) 獲取緩存中key對應的value,如果緩存沒命中,返回null。return value if cached, otherwise return null.
  • V get(K key) throws ExecutionException 獲取key對應的value,若緩存中沒有,則調用LocalCache的load方法,從數據源中加載,並緩存。 return value if cached, otherwise load, cache and return.
  • void put(K key, V value) if cached, return; otherwise create, cache , and return.
  • void invalidate(Object key); 刪除緩存
  • void invalidateAll(); 清楚所有的緩存,相當遠map的clear操作。
  • long size(); 獲取緩存中元素的大概個數。為什么是大概呢?元素失效之時,並不會實時的更新size,所以這里的size可能會包含失效元素。
  • CacheStats stats(); 緩存的狀態數據,包括(未)命中個數,加載成功/失敗個數,總共加載時間,刪除個數等。
  • ConcurrentMap<K, V> asMap(); 將緩存存儲到一個線程安全的map中。

批量操作就是循環調用上面對應的方法,如:

  • ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
  • void putAll(Map<? extends K,? extends V> m);
  • void invalidateAll(Iterable<?> keys);

示例:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.TimeUnit;

public class guavaSample {

    public static void main(String[] args) {

        LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                .maximumSize(10)  //最多存放十個數據
                .expireAfterWrite(10, TimeUnit.SECONDS)  //緩存200秒
                .recordStats()   //開啟 記錄狀態數據功能
                .build(new CacheLoader<String, Integer>() {
                    //數據加載,默認返回-1,也可以是查詢操作,如從DB查詢
                    @Override
                    public Integer load(String key) throws Exception {
                        return -1;
                    }
                });

        //只查詢緩存,沒有命中,即返回null。 miss++
        System.out.println(cache.getIfPresent("key1")); //null
        //put數據,放在緩存中
        cache.put("key1", 1);
        //再次查詢,已存在緩存中, hit++
        System.out.println(cache.getIfPresent("key1")); //1
        //失效緩存
        cache.invalidate("key1");
        //失效之后,查詢,已不在緩存中, miss++
        System.out.println(cache.getIfPresent("key1")); //null
        
        try{
            //查詢緩存,未命中,調用load方法,返回-1. miss++
            System.out.println(cache.get("key2"));   //-1
            //put數據,更新緩存
            cache.put("key2", 2);
            //查詢得到最新的數據, hit++
            System.out.println(cache.get("key2"));    //2
            System.out.println("size :" + cache.size());  //1

            //插入十個數據
            for(int i=3; i<13; i++){
                cache.put("key"+i, i);
            }
            //超過最大容量的,刪除最早插入的數據,size正確
            System.out.println("size :" + cache.size());  //10
            //miss++
            System.out.println(cache.getIfPresent("key2"));  //null

            Thread.sleep(5000); //等待5秒
            cache.put("key1", 1);
            cache.put("key2", 2);
            //key5還沒有失效,返回5。緩存中數據為key1,key2,key5-key12. hit++
            System.out.println(cache.getIfPresent("key5")); //5

            Thread.sleep(5000); //等待5秒
            //此時key5-key12已經失效,但是size沒有更新
            System.out.println("size :" + cache.size());  //10
            //key1存在, hit++
            System.out.println(cache.getIfPresent("key1")); //1
            System.out.println("size :" + cache.size());  //10
            //獲取key5,發現已經失效,然后刷新緩存,遍歷數據,去掉失效的所有數據, miss++
            System.out.println(cache.getIfPresent("key5")); //null
            //此時只有key1,key2沒有失效
            System.out.println("size :" + cache.size()); //2

            System.out.println("status, hitCount:" + cache.stats().hitCount()
                + ", missCount:" + cache.stats().missCount()); //4,5
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}

guava cache源碼解析

先了解一些主要類和接口:

  • CacheBuilder:類,緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。
    CacheBuilder在build方法中,會把前面設置的參數,全部傳遞給LocalCache,它自己實際不參與任何計算。這種初始化參數的方法值得借鑒,代碼簡潔易讀。
  • CacheLoader:抽象類。用於從數據源加載數據,定義load、reload、loadAll等操作。
  • Cache:接口,定義get、put、invalidate等操作,這里只有緩存增刪改的操作,沒有數據加載的操作。
  • AbstractCache:抽象類,實現Cache接口。其中批量操作都是循環執行單次行為,而單次行為都沒有具體定義。
  • LoadingCache:接口,繼承自Cache。定義get、getUnchecked、getAll等操作,這些操作都會從數據源load數據。
  • AbstractLoadingCache:抽象類,繼承自AbstractCache,實現LoadingCache接口。
  • LocalCache:類。整個guava cache的核心類,包含了guava cache的數據結構以及基本的緩存的操作方法。
  • LocalManualCache:LocalCache內部靜態類,實現Cache接口。其內部的增刪改緩存操作全部調用成員變量localCache(LocalCache類型)的相應方法。
  • LocalLoadingCache:LocalCache內部靜態類,繼承自LocalManualCache類,實現LoadingCache接口。其所有操作也是調用成員變量localCache(LocalCache類型)的相應方法。

綜上,guava cache的核心操作,都在LocalCache中實現。
其他:

  • CacheStats:緩存加載/命中統計信息。

在看具體的代碼之前,先來簡單了解一下LocalCache的數據結構。
LocalCache的數據結構如下所示:
guava cache

LocalCache的數據結構與ConcurrentHashMap很相似,都由多個segment組成,且各segment相對獨立,互不影響,所以能支持並行操作。每個segment由一個table和若干隊列組成。緩存數據存儲在table中,其類型為AtomicReferenceArray<ReferenceEntry<K, V>>,即一個數組,數組中每個元素是一個鏈表。兩個隊列分別是writeQueue和accessQueue,用來存儲寫入的數據和最近訪問的數據,當數據過期,需要刷新整體緩存(見上述示例最后一次cache.getIfPresent("key5"))時,遍歷隊列,如果數據過期,則從table中刪除。segment中還有基於引用場景的其他隊列,這里先不做討論。

CacheBuilder

CacheBuilder是緩存配置和構建入口,先看一些屬性。CacheBuilder的設置操作都是為這些屬性賦值。

    //緩存的默認初始化大小
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    // LocalCache默認並發數,用來評估Segment的個數
    private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
    //默認的緩存過期時間
    private static final int DEFAULT_EXPIRATION_NANOS = 0;
    
    static final int UNSET_INT = -1;
    int initialCapacity = UNSET_INT;//初始緩存大小
    int concurrencyLevel = UNSET_INT;//用於計算有幾個並發
    long maximumSize = UNSET_INT;//cache中最多能存放的緩存entry個數
    long maximumWeight = UNSET_INT;
    
    Strength keyStrength;//鍵的引用類型(strong、weak、soft)
    Strength valueStrength;//值的引用類型(strong、weak、soft)

    long expireAfterWriteNanos = UNSET_INT;//緩存超時時間(起點:緩存被創建或被修改)
    long expireAfterAccessNanos = UNSET_INT;//緩存超時時間(起點:緩存被創建或被修改或被訪問)
    //元素被移除的監聽器
     RemovalListener<? super K, ? super V> removalListener;
     //狀態計數器,默認為NULL_STATS_COUNTER,即不啟動計數功能
     Supplier<? extends StatsCounter> statsCounterSupplie

CacheBuilder構建緩存有兩個方法:

//構建一個具有數據加載功能的緩存
  public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
    checkWeightWithWeigher();
    //調用LocalCache構造方法
    return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
  }
  
  //構建一個沒有數據加載功能的緩存
   public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    checkWeightWithWeigher();
    checkNonLoadingCache();
    //調用LocalCache構造方法,但loader為null
    return new LocalCache.LocalManualCache<K1, V1>(this);
  }

 //被CacheBuilder的build方法調用,將其參數傳遞給LocalCache
  LocalCache(
      CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
    //默認並發水平是4,即四個Segment(但要注意concurrencyLevel不一定等於Segment個數)
    //Segment個數:一個剛剛大於或等於concurrencyLevel且是2的幾次方的一個數,在后面會有segmentCount賦值過程    
    concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);

    keyStrength = builder.getKeyStrength();//默認為Strong,即強引用
    valueStrength = builder.getValueStrength();//默認為Strong,即強引用

    keyEquivalence = builder.getKeyEquivalence();
    valueEquivalence = builder.getValueEquivalence();

    maxWeight = builder.getMaximumWeight();
    weigher = builder.getWeigher();
    expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
    expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
    refreshNanos = builder.getRefreshNanos();

    removalListener = builder.getRemovalListener();
    removalNotificationQueue = (removalListener == NullListener.INSTANCE)
        ? LocalCache.<RemovalNotification<K, V>>discardingQueue()
        : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();

    ticker = builder.getTicker(recordsTime());
    entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
    globalStatsCounter = builder.getStatsCounterSupplier().get();
    defaultLoader = loader;

    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
    if (evictsBySize() && !customWeigher()) {
      initialCapacity = Math.min(initialCapacity, (int) maxWeight);
    }

   //調整segmentCount個數,通過位移實現,所以是2的n次方。
    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel
           && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      ++segmentShift;
      segmentCount <<= 1;
    }
    this.segmentShift = 32 - segmentShift;
    segmentMask = segmentCount - 1;
    //初始化segments
    this.segments = newSegmentArray(segmentCount);
    //每個segment的大小
    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }

    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }
	//初始化Segments
    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }
  }

    //Segment初始化操作,結構與上面圖中大致相同(圖中省去部分隊列)
    Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight,
        StatsCounter statsCounter) {
      this.map = map;
      this.maxSegmentWeight = maxSegmentWeight;
      this.statsCounter = checkNotNull(statsCounter);
      //初始化table
      initTable(newEntryArray(initialCapacity));
     //key引用隊列
      keyReferenceQueue = map.usesKeyReferences()
           ? new ReferenceQueue<K>() : null;
     //value引用隊列
      valueReferenceQueue = map.usesValueReferences()
           ? new ReferenceQueue<V>() : null;

      recencyQueue = map.usesAccessQueue()
          ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
      //寫入元素隊列
      writeQueue = map.usesWriteQueue()
          ? new WriteQueue<K, V>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
     //訪問元素隊列
      accessQueue = map.usesAccessQueue()
          ? new AccessQueue<K, V>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
    }

LocalCache

LocalCache是guava cache的核心類。LocalCache的構造函數在上面已經分析過,接着看下核心方法。

對於get(key, loader)方法流程:

  • 對key做hash,找到存儲的segment及數組table上的位置;
  • 鏈表上查找entry,如果entry不為空,且value沒有過期,則返回value,並刷新entry。
  • 若鏈表上找不到entry,或者value已經過期,則調用lockedGetOrLoad。
  • 鎖住整個segment,遍歷entry可能在的鏈表,查看數據是否存在是否過期,若存在則返回。若過期則刪除(table,各種queue)。若不存在,則新建一個entry插入table。放開整個segment的鎖。
  • 鎖住entry,調用loader的reload方法,從數據源加載數據,然后調用storeLoadedValue更新緩存。
  • storeLoadedValue時,鎖住整個segment,將value設置到entry中,並設置相關數據(入寫入/訪問隊列,加載/命中數據等)。

getAll(keys)方法:

  • 循環調用get方法,從緩存中獲取key對應的value。沒有命中的記錄下來。
  • 如果有沒有命中的key,調用loadAll(keys,loader)方法加載數據。
  • 將加載的數據依次緩存,調用segment的put(K key, int hash, V value, boolean onlyIfAbsent)方法。
  • put時,鎖住整個segment,將數據插入鏈表,更新統計數據。

put(key,value)方法:

  • 對key做hash,找到segment的位置和table上的位置;
  • 鎖住整個segment,將數據插入鏈表,更新統計數據。

putAll(map) 循環調用put方法。

putIfAbsent(key, value) 緩存中,鍵值對不存在的時候才插入。

實踐

guava cache是將數據源中的數據緩存在本地,那如果我們想把遠端數據源中的數據緩存在遠端 分布式緩存(如redis),可以怎么來使用guava cache的方式進行封裝呢?
可以仿照guava寫一個簡單的緩存,定義如下:
CacheBuilder類 : 配置緩存參數,構建緩存。同上面所講。
Cache接口:定義增刪查接口。
MyCache類:實現Cache接口,put -> 存入DB,更新緩存; get -> 查詢緩存,存在即返回;若不存在,查詢DB,更新緩存,返回。
CacheLoader類:供MyCache調用,get和getAll時提供單次查DB和批量查DB。

參考

Google guava cache源碼解析1--構建緩存器
Google Guava 緩存

后續

guava cache基於引用回收相關;
刪除監聽器相關。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM