- 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。
參考
Google guava cache源碼解析1--構建緩存器
Google Guava 緩存
后續
guava cache基於引用回收相關;
刪除監聽器相關。