guava cache(LoadingCache)使用和源碼分析


  • 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的數據結構如下所示:

    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。


免責聲明!

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



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