一.概述
在上篇文章《Guava Cache做本地緩存那些事》中我介紹了Guava Cache作為本地緩存的一般用法,叙述了構造LoadingCache的方式和其基本使用,以及兩種get方法的差別,LoadingCache和Cache的差別等内容。而為了知其然并知其是以然,本文對Guava Cache的實作原理做一些簡單介紹,如有不對的地方請大家批評指正。
首先,先把Guava Cache在github上的源碼連結放出來。裡面有作者寫的注釋,可以友善大家學習。
Guava Cache帶注釋源碼
二.關注的問題
從上述源碼和前一篇介紹Guava Cache用法的文章中大家可以看出Guava Cache涉及的類和代碼還是比較多的,如果挨個檔案去分析涉及的細節太多,不容易把握關鍵的問題。是以筆者決定從get方法的執行過程出發對Guava Cache的原理進行分析。分析主要關注以下三個問題:
- Guava Cache是如何通過key映射到value的;
- 未命中緩存時如何調用的CacheLoader.load方法來載入緩存的;
- 兩種緩存失效政策是如何實作的。
三.源碼分析
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判斷過期并不是單獨再開一個線程,而是在讀寫操作時順帶着進行了過期判斷。
關注公衆号,第一時間接收我更多文章