天天看點

Guava Cache實作原理淺析

一.概述

在上篇文章《Guava Cache做本地緩存那些事》中我介紹了Guava Cache作為本地緩存的一般用法,叙述了構造LoadingCache的方式和其基本使用,以及兩種get方法的差別,LoadingCache和Cache的差別等内容。而為了知其然并知其是以然,本文對Guava Cache的實作原理做一些簡單介紹,如有不對的地方請大家批評指正。

首先,先把Guava Cache在github上的源碼連結放出來。裡面有作者寫的注釋,可以友善大家學習。

Guava Cache帶注釋源碼

二.關注的問題

從上述源碼和前一篇介紹Guava Cache用法的文章中大家可以看出Guava Cache涉及的類和代碼還是比較多的,如果挨個檔案去分析涉及的細節太多,不容易把握關鍵的問題。是以筆者決定從get方法的執行過程出發對Guava Cache的原理進行分析。分析主要關注以下三個問題:

  1. Guava Cache是如何通過key映射到value的;
  2. 未命中緩存時如何調用的CacheLoader.load方法來載入緩存的;
  3. 兩種緩存失效政策是如何實作的。

三.源碼分析

3.1 key-value的映射過程

通過以下例子我們來看一下key-value的映射過程:

public static void demo1() throws Exception {
    	//定義Cache,并定義未命中時的載入方法
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().build(new CacheLoader<Integer, Integer>() {
            @Override
            public Integer load(Integer key) throws Exception {
                return key + 1;
            }
        });
        cache.get(1); //第一個get:第一次通路key,使用載入方法載入
        cache.get(1); //第二個get:第二次通路,在緩存中得到value
    }
           

以上demo定義了一個LoadingCache,并傳入了CacheLoader提供在未命中緩存時的處理方法。後面可以看到對get方法調用了兩次,第一次是為了通過load方法将相應的value載入緩存,這個過程在3.2節中介紹。本節主要看一下第二次get的過程:在緩存中存在對應value的情況下,如何通過key拿到value的。

通過debug斷點,我們進入get方法,觀察其執行過程:

step1:

首先進入的是如下這個方法,可以推斷它所在的LoaclLoadingCache是LoadingCache接口的預設實作。這個類是LoaclCache的一個内部類。get方法直接調用了getOrLoad方法。

static class LocalLoadingCache<K, V> extends LocalCache.LocalManualCache<K, V> implements LoadingCache<K, V> {
        public V get(K key) throws ExecutionException {
            return this.localCache.getOrLoad(key);
        }
}
           

step2:

我們繼續往下看getOrLoad方法,發現它也隻是一層封裝,go on!

V getOrLoad(K key) throws ExecutionException {
        return this.get(key, this.defaultLoader);
    }
           

step3:

繼續往下追蹤到LocalCache.get方法。到這裡終于可以看出一些操作了。一共有兩條語句,分别分析下。

第一句是求哈希值,其中Preconditions.checkNotNull方法用于檢查空指針,如果key是null的話就抛出空指針異常,然後進行hash操作。hash操作時,先取得key.hashCode()的傳回值,再進行rehash操作。rehash的目的是為了防禦由key.hashCode()品質差引起的hash沖突嚴重問題。個人了解的其具體做法是進行一系列位運算,将高位和低位的hash值進行混淆。

第二句是我們分析的重點,請看step4。

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = this.hash(Preconditions.checkNotNull(key));
        return this.segmentFor(hash).get(key, hash, loader);
    }
           

step4:

LoadingCache采用了類似ConcurrentHashMap的方式,将映射表分為多個segment。segment之間可以并發通路,這樣可以大大提高并發的效率,使得并發沖突的可能性降低了(同時通路一個segment上的key時還是有可能沖突的)。

可以将step3中最後一行代碼分為兩部分,一是this.segmentFor(hash),這是通過hash值确定該key位于哪一個segment上,并擷取該segment。而是再通過segment執行get方法來擷取具體的value值。

首先看一下this.segmentFor(hash)的代碼:

LocalCache.Segment<K, V> segmentFor(int hash) {
    return this.segments[hash >>> this.segmentShift & this.segmentMask];
}
           

可以看出segments是存儲在一個數組中的,該數組的長度是2的次幂,同時等于LoadingCache的并發等級。那麼segmentShift和segmentMask是如何取值的呢?可以将segmentMash了解為數組長度減一,這樣和它進行與操作後的範圍總是在數組的長度範圍内。segmentShift的取值有興趣的可以參考源碼LocalCache的構造方法了解,這裡不再贅述。

取得segment後,便可以執行我們最終的get方法了。其核心主要代碼如下:

//count記錄目前segment中存活的緩存個數
       if (this.count != 0) {
       	   //其實這一句就已經擷取到了value值,下面的代碼進行了過期判斷等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判斷是否過期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未載入的情況,3.2節進行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;
           

代碼ReferenceEntry<K, V> e = this.getEntry(key, hash);就擷取到了key相關的鍵值對,後續代碼進行了過期政策(3.3節介紹)和載入操作(3.2介紹)等操作。這裡主要看一下getEntry方法。

和HashMap等類似,segment也采用了數組加連結清單的資料結構。首先由hash值和數組長度減一進行與操作擷取連結清單的頭結點,再周遊連結清單得到真正的結果。

小結

以上就是對LocalCache的get方法的分析,可以看出其實作方法除了一些細節上的不同基本和ConcurrentHashMap等Java标準類庫中的實作類似。從分析的step4我們也知道了進行get操作時會進行CacheLoader.load的調用和緩存失效的判斷,下面就具體再來看一下這兩塊内容的原理。

3.2 CacheLoader.load的調用機制

先回顧一下上篇文章知識,CacheLoader.load是在建構LoadingCache時傳入給CacheBuilder的,并在緩存未命中的情況下進行調用。在3.1節step4中我們看到了Load被調用的入口點,為了閱讀友善,這裡再把該塊代碼貼一下:

//count記錄目前segment中存活的緩存個數
       if (this.count != 0) {
       	   //其實這一句就已經擷取到了value值,下面的代碼進行了過期判斷等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判斷是否過期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未載入的情況,3.2節進行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;
           

可以看到當this.count==0時就會執行倒數第二行的this.lockedGetOrLoad方法,在這個方法裡就對 CacheLoader.load進行了調用。由于這裡是寫操作,需要對segment進行加鎖。segment是ReentrantLock的子類,直接調用其lock()方法即可加鎖。之後對CacheLoader.load()進行調用擷取value值,并放入table中的連結清單裡。具體的代碼較為繁瑣就不再分析了。

3.3 兩種緩存過期政策的實作

Guava Cache可以設定兩種過期政策,一是機關時間内沒有讀緩存過期,另一種是機關時間内沒有寫緩存過期。

判斷是否過期的入口在3.1節step4中提到的segment.get中:

//count記錄目前segment中存活的緩存個數
       if (this.count != 0) {
       	   //其實這一句就已經擷取到了value值,下面的代碼進行了過期判斷等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判斷是否過期
               V value = this.getLiveValue(e, now);
               ……
           }
           ……
       }
           

一路追蹤getLiveValue方法,找到了以下判斷的位置:

boolean isExpired(ReferenceEntry<K, V> entry, long now) {
        Preconditions.checkNotNull(entry);
        if (this.expiresAfterAccess() && now - entry.getAccessTime() >= this.expireAfterAccessNanos) {
            return true;
        } else {
            return this.expiresAfterWrite() && now - entry.getWriteTime() >= this.expireAfterWriteNanos;
        }
    }
           

可以看到根據過期政策、目前的時間、設定的過期時間進行了是否過期判斷。同時我們也知道了Guava Cache判斷過期并不是單獨再開一個線程,而是在讀寫操作時順帶着進行了過期判斷。

關注公衆号,第一時間接收我更多文章

Guava Cache實作原理淺析

繼續閱讀