使用Guava?Cache原理及最佳實(shí)踐
緩存的種類有很多,需要根據(jù)不同的應(yīng)用場(chǎng)景來(lái)選擇不同的cache,比如分布式緩存如redis、memcached,還有本地(進(jìn)程內(nèi))緩存如:ehcache、GuavaCache、Caffeine。
本篇主要圍繞全內(nèi)存緩存-Guava Cache做一些詳細(xì)的講解和分析。
1. Guava Cache是什么
1.1 簡(jiǎn)介
Guava cache是一個(gè)支持高并發(fā)的線程安全的本地緩存。多線程情況下也可以安全的訪問或者更新Cache。這些都是借鑒了ConcurrentHashMap的結(jié)果,不過,guava cache 又有自己的特性 :
"automatic loading of entries into the cache"
即 :當(dāng)cache中不存在要查找的entry的時(shí)候,它會(huì)自動(dòng)執(zhí)行用戶自定義的加載邏輯,加載成功后再將entry存入緩存并返回給用戶未過期的entry,如果不存在或者已過期,則需要load,同時(shí)為防止多線程并發(fā)下重復(fù)加載,需要先鎖定,獲得加載資格的線程(獲得鎖的線程)創(chuàng)建一個(gè)LoadingValueRefrerence并放入map中,其他線程等待結(jié)果返回。
1.2 核心功能
- 自動(dòng)將entry節(jié)點(diǎn)加載進(jìn)緩存結(jié)構(gòu)中;
- 當(dāng)緩存的數(shù)據(jù)超過設(shè)置的最大值時(shí),使用LRU算法移除;
- 具備根據(jù)entry節(jié)點(diǎn)上次被訪問或者寫入時(shí)間計(jì)算它的過期機(jī)制;
- 緩存的key被封裝在
WeakReference引用內(nèi); - 緩存的Value被封裝在
WeakReference或SoftReference引用內(nèi); - 統(tǒng)計(jì)緩存使用過程中命中率、異常率、未命中率等統(tǒng)計(jì)數(shù)據(jù)。
小結(jié):Guava Cache說簡(jiǎn)單點(diǎn)就是一個(gè)支持LRU的ConcurrentHashMap,并提供了基于容量,時(shí)間和引用的緩存回收方式。(簡(jiǎn)單概括)
1.3 適用場(chǎng)景
- 愿意消耗一些內(nèi)存空間來(lái)提升速度(以空間換時(shí)間,提升處理速度);
- 能夠預(yù)計(jì)某些key會(huì)被查詢一次以上;
- 緩存中存放的數(shù)據(jù)總量不會(huì)超出內(nèi)存容量(
Guava Cache是單個(gè)應(yīng)用運(yùn)行時(shí)的本地緩存)。
- 計(jì)數(shù)器(如可以利用基于時(shí)間的過期機(jī)制作為限流計(jì)數(shù))
2. Guava Cache的使用
GuavaCache使用時(shí)主要分二種模式:LoadingCache、CallableCache
核心區(qū)別在于:LoadingCache創(chuàng)建時(shí)需要有合理的默認(rèn)方法來(lái)加載或計(jì)算與鍵關(guān)聯(lián)的值,CallableCache創(chuàng)建時(shí)無(wú)需關(guān)聯(lián)固定的CacheLoader使用起來(lái)更加靈活。
前置準(zhǔn)備:
- 引入jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>- 了解CacheBuilder的配置方法

- mock RPC調(diào)用方法,用于獲取數(shù)據(jù)
private static List<String> rpcCall(String cityId) {
// 模仿從數(shù)據(jù)庫(kù)中取數(shù)據(jù)
try {
switch (cityId) {
case "0101":
System.out.println("load cityId:" + cityId);
return ImmutableList.of("上海", "北京", "廣州", "深圳");
}
} catch (Exception e) {
// 記日志
}
return Collections.EMPTY_LIST;
}2.1 創(chuàng)建LoadingCache緩存
使用CacheBuilder來(lái)構(gòu)建LoadingCache實(shí)例,可以鏈?zhǔn)秸{(diào)用多個(gè)方法來(lái)配置緩存的行為。其中CacheLoader可以理解為一個(gè)固定的加載器,在創(chuàng)建LoadingCache時(shí)指定,然后簡(jiǎn)單地重寫V load(K key) throws Exception方法,就可以達(dá)到當(dāng)檢索不存在的時(shí)候自動(dòng)加載數(shù)據(jù)的效果。
//創(chuàng)建一個(gè)LoadingCache,并可以進(jìn)行一些簡(jiǎn)單的緩存配置
private static LoadingCache<String, Optional<List<String>> > loadingCache = CacheBuilder.newBuilder()
//配置最大容量為100,基于容量進(jìn)行回收
.maximumSize(100)
//配置寫入后多久使緩存過期-下文會(huì)講述
.expireAfterWrite(3, TimeUnit.SECONDS)
//配置寫入后多久刷新緩存-下文會(huì)講述
.refreshAfterWrite(3, TimeUnit.SECONDS)
//key使用弱引用-WeakReference
.weakKeys()
//當(dāng)Entry被移除時(shí)的監(jiān)聽器-下文會(huì)講述
.removalListener(notification -> System.out.println("notification=" + notification))
//創(chuàng)建一個(gè)CacheLoader,重寫load方法,以實(shí)現(xiàn)"當(dāng)get時(shí)緩存不存在,則load,放到緩存并返回的效果
.build(new CacheLoader<String, Optional<List<String>>>() {
//重點(diǎn),自動(dòng)寫緩存數(shù)據(jù)的方法,必須要實(shí)現(xiàn)
@Override
public Optional<List<String>> load(String cityId) throws Exception {
return Optional.ofNullable(rpcCall(cityId));
}
//異步刷新緩存-下文會(huì)講述
@Override
public ListenableFuture<Optional<List<String>>> reload(String cityId, Optional<List<String>> oldValue) throws Exception {
return super.reload(cityId, oldValue);
}
});
// 測(cè)試
public static void main(String[] args) {
try {
System.out.println("load from cache once : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
Thread.sleep(4000);
System.out.println("load from cache two : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
Thread.sleep(2000);
System.out.println("load from cache three : " + loadingCache.get("0101").orElse(Lists.newArrayList()));
Thread.sleep(2000);
System.out.println("load not exist key from cache : " + loadingCache.get("0103").orElse(Lists.newArrayList()));
} catch (ExecutionException | InterruptedException e) {
//記錄日志
}
}執(zhí)行結(jié)果

2.2 創(chuàng)建CallableCache緩存
在上面的build方法中是可以不用創(chuàng)建CacheLoader的,不管有沒有CacheLoader,都是支持Callable的。Callable在get時(shí)可以指定,效果跟CacheLoader一樣,區(qū)別就是兩者定義的時(shí)間點(diǎn)不一樣,Callable更加靈活,可以理解為Callable是對(duì)CacheLoader的擴(kuò)展。CallableCache的方式最大的特點(diǎn)在于可以在get的時(shí)候動(dòng)態(tài)的指定load的數(shù)據(jù)源
//創(chuàng)建一個(gè)callableCache,并可以進(jìn)行一些簡(jiǎn)單的緩存配置
private static Cache<String, Optional<List<String>>> callableCache = CacheBuilder.newBuilder()
//最大容量為100(基于容量進(jìn)行回收)
.maximumSize(100)
//配置寫入后多久使緩存過期-下文會(huì)講述
.expireAfterWrite(3, TimeUnit.SECONDS)
//key使用弱引用-WeakReference
.weakKeys()
//當(dāng)Entry被移除時(shí)的監(jiān)聽器
.removalListener(notification -> System.out.println("notification=" + notification))
//不指定CacheLoader
.build();
// 測(cè)試
public static void main(String[] args) {
try {
System.out.println("load from callableCache once : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
Thread.sleep(4000);
System.out.println("load from callableCache two : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
Thread.sleep(2000);
System.out.println("load from callableCache three : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));
Thread.sleep(2000);
System.out.println("load not exist key from callableCache : " + callableCache.get("0103", () -> Optional.ofNullable(rpcCall("0103"))).orElse(Lists.newArrayList()));
} catch (ExecutionException | InterruptedException e) {
//記錄日志
}
}執(zhí)行結(jié)果:

2.3 其他用法
// 聲明一個(gè)CallableCache,不需要CacheLoader
private static Cache<String, Optional<List<String>>> localCache = CacheBuilder
.newBuilder()
.maximumSize(100)
.expireAfterAccess(10, TimeUnit.MINUTES)
.removalListener(notification -> System.out.println("notification=" + notification))
.build();
// 測(cè)試。使用時(shí)自主控制get、put等操作
public static void main(String[] args) {
try {
String cityId = "0101";
Optional<List<String>> ifPresent1 = localCache.getIfPresent(cityId);
System.out.println("load from localCache one : " + ifPresent1);
// 做判空,不存在時(shí)手工獲取并put數(shù)據(jù)到localCache中
if (ifPresent1 == null || ifPresent1.isPresent() || CollectionUtils.isEmpty(ifPresent1.get())) {
List<String> stringList = rpcCall(cityId);
if (CollectionUtils.isNotEmpty(stringList)) {
localCache.put(cityId, Optional.ofNullable(stringList));
}
}
Optional<List<String>> ifPresent2 = localCache.getIfPresent(cityId);
System.out.println("load from localCache two : " + ifPresent2);
// 失效某個(gè)key,或者loadingCache.invalidateAll() 方法
localCache.invalidate(cityId);
Optional<List<String>> ifPresent3 = localCache.getIfPresent(cityId);
System.out.println("load from localCache three : " + ifPresent3);
} catch (Exception e) {
throw new RuntimeException(e);
}
}執(zhí)行結(jié)果

通過上面三個(gè)案例的講解,相信大家對(duì)于guava cache的使用應(yīng)該沒啥問題了,接下來(lái)一起學(xué)習(xí)緩存的失效機(jī)制!
3.緩存失效回收策略
前面說到Guava Cache與ConcurrentHashMap很相似,包括其并發(fā)策略,數(shù)據(jù)結(jié)構(gòu)等,但也不完全一樣。
最基本的區(qū)別是ConcurrentHashMap會(huì)一直保存所有添加的元素,直到顯式地移除,而guava cache可以自動(dòng)回收元素,在某種情況下Guava Cache 會(huì)根據(jù)一定的算法自動(dòng)移除一些條目,以確保緩存不會(huì)占用太多內(nèi)存,避免內(nèi)存浪費(fèi)。
3.1 基于容量回收
基于容量的回收是一種常用策略。在構(gòu)建緩存時(shí)使用 CacheBuilder 的 maximumSize 方法來(lái)設(shè)置緩存的最大條目數(shù)。
當(dāng)緩存中的條目數(shù)量超過了最大值時(shí),Guava Cache 會(huì)根據(jù)LRU(最近最少使用)算法來(lái)移除一些條目。例如:
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
// 緩存最多可以存儲(chǔ)1000個(gè)條目
.maximumSize(1000)
.build();除了 maximumSize,Guava Cache 還提供了 maximumWeight 方法和 weigher 方法,允許你根據(jù)每個(gè)條目的權(quán)重來(lái)限制緩存,而不是簡(jiǎn)單的條目數(shù)量。
這在緩存的條目大小不一致時(shí)特別有用。需要注意的是,淘汰的順序仍然是根據(jù)條目的訪問順序,而不是權(quán)重大小。 例如:
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
// 緩存最多可以存儲(chǔ)的總權(quán)重
.maximumWeight(10000)
.weigher(new Weigher<KeyType, ValueType>() {
public int weigh(KeyType key, ValueType value) {
// 定義如何計(jì)算每個(gè)條目的權(quán)重
return getSizeInBytes(key, value);
}
})
.build();注意事項(xiàng):
1、權(quán)重是在緩存創(chuàng)建時(shí)計(jì)算的,因此要考慮權(quán)重計(jì)算的復(fù)雜度。
3.2 定時(shí)回收
Guava Cache提供了兩種基于時(shí)間的回收策略。
- 基于寫操作的回收(expireAfterWrite)
使用 expireAfterWrite 方法設(shè)置的緩存條目在給定時(shí)間內(nèi)沒有被寫訪問(創(chuàng)建或覆蓋),則會(huì)被回收。這種策略適用于當(dāng)信息在一段時(shí)間后就不再有效或變得陳舊時(shí)。 例如,下面的代碼創(chuàng)建了一個(gè)每當(dāng)條目在30分鐘內(nèi)沒有被寫訪問(創(chuàng)建或覆蓋)就會(huì)過期的緩存:
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();- 基于訪問操作的回收(expireAfterAccess)
使用 expireAfterAccess 方法設(shè)置的緩存條目在給定時(shí)間內(nèi)沒有被讀取或?qū)懭?,則會(huì)被回收。這種策略適用于需要回收那些可能很長(zhǎng)時(shí)間都不會(huì)被再次使用的條目。 例如,下面的代碼創(chuàng)建了一個(gè)每當(dāng)條目在15分鐘內(nèi)沒有被訪問(讀取或?qū)懭耄┚蜁?huì)過期的緩存:
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
.expireAfterAccess(15, TimeUnit.MINUTES)
.build();3.3 基于引用回收
Guava Cache 提供了基于引用的回收機(jī)制,這種機(jī)制允許緩存通過使用弱引用(weak references)或軟引用(soft references)來(lái)存儲(chǔ)鍵(keys)或值(values),以便在內(nèi)存緊張時(shí)能夠自動(dòng)回收這些緩存條目。
- 弱引用鍵(Weak Keys)
使用 weakKeys() 方法配置的緩存會(huì)對(duì)鍵使用弱引用。當(dāng)鍵不再有其他強(qiáng)引用時(shí),即使它還在緩存中,也可能被垃圾回收器回收。
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
.weakKeys()
.build();弱引用鍵的緩存主要用于緩存鍵是可丟棄的或由外部系統(tǒng)管理生命周期的對(duì)象。例如,緩存外部資源的句柄,當(dāng)句柄不再被應(yīng)用程序使用時(shí),可以安全地回收。
- 軟引用值(Soft Values)
使用 softValues() 方法配置的緩存會(huì)對(duì)值使用軟引用。軟引用對(duì)象在內(nèi)存充足時(shí)會(huì)保持不被回收,但在JVM內(nèi)存不足時(shí),軟引用對(duì)象可能被垃圾回收器回收。
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
.softValues()
.build();軟引用值的緩存適合用于緩存占用內(nèi)存較大的對(duì)象,例如圖片或文檔數(shù)據(jù)。當(dāng)應(yīng)用程序內(nèi)存需求增加時(shí),這些大對(duì)象可以被回收以釋放內(nèi)存。
注意事項(xiàng):
1、基于引用的回收策略不是由緩存大小或元素的存活時(shí)間決定的,而是與JVM的垃圾回收機(jī)制緊密相關(guān),而垃圾回收的行為會(huì)受到JVM配置和當(dāng)前內(nèi)存使用情況的影響,因此,引用回收策略下緩存回收具有不確定性,會(huì)導(dǎo)致緩存行為的不可預(yù)測(cè)性。
2、基于引用的回收策略通常不應(yīng)與需要精確控制內(nèi)存占用的場(chǎng)景混用。在使用基于引用的回收策略時(shí),應(yīng)該仔細(xì)考慮應(yīng)用程序的內(nèi)存需求和垃圾回收行為,以確保緩存能夠按照預(yù)期工作。
3.4 顯式清除
Guava Cache 提供了幾種顯式清除緩存條目的方法,允許你手動(dòng)移除緩存中的某個(gè)或某些條目。
- 移除單個(gè)條目
使用 invalidate(key) 方法可以移除緩存中的特定鍵對(duì)應(yīng)的條目。
cache.invalidate(key);
- 移除多個(gè)條目
使用 invalidateAll(keys) 方法可以移除緩存中所有在給定集合中的鍵對(duì)應(yīng)的條目。
cache.invalidateAll(keys);
- 移除所有條目
使用 invalidateAll() 方法可以移除緩存中的所有條目。
cache.invalidateAll();
- 使用 Cache.asMap() 視圖進(jìn)行移除
通過緩存的 asMap() 方法獲取的 ConcurrentMap 視圖,你可以使用 Map 接口提供的方法來(lái)移除條目。
// 移除單個(gè)條目
cache.asMap().remove(key);
// 批量移除條目
for (KeyType key : keys) {
cache.asMap().remove(key);
}
// 移除滿足特定條件的條目
cache.asMap().entrySet().removeIf(entry -> entry.getValue().equals(someValue));注意事項(xiàng):
asMap 視圖提供了緩存的 ConcurrentMap 形式,這種方式在使用時(shí)和直接操作緩存的交互有區(qū)別,如下:
1、cache.asMap()包含當(dāng)前所有加載到緩存的項(xiàng)。因此cache.asMap().keySet()包含當(dāng)前所有已加載鍵;
2、asMap().get(key)實(shí)質(zhì)上等同于 cache.getIfPresent(key),而且不會(huì)引起緩存項(xiàng)的加載。這和 Map 的語(yǔ)義約定一致。
3、所有讀寫操作都會(huì)重置相關(guān)緩存項(xiàng)的訪問時(shí)間,包括 Cache.asMap().get(Object)方法和 Cache.asMap().put(K, V)方法,但不包括 Cache.asMap().containsKey(Object)方法,也不包括在 Cache.asMap()的集合視圖上的操作。比如,遍歷 Cache.asMap().entrySet()不會(huì)重置緩存項(xiàng)的讀取時(shí)間。
- 注冊(cè)移除監(jiān)聽器
可以在構(gòu)建緩存時(shí)注冊(cè)一個(gè)移除監(jiān)聽器(RemovalListener),它會(huì)在每次條目被移除時(shí)調(diào)用。
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
.removalListener(new RemovalListener<KeyType, ValueType>() {
@Override
public void onRemoval(RemovalNotification<KeyType, ValueType> notification) {
// 處理移除事件
}
})
.build();在實(shí)際項(xiàng)目實(shí)踐中,往往是多種回收策略一起使用,讓Guava Cache緩存提供多層次的回收保障。
4、緩存失效回收時(shí)機(jī)
緩存回收策略講清楚后,那么這些策略到底是在什么時(shí)候觸發(fā)的呢?我們直接說結(jié)論:
Guava Cache基于容量和時(shí)間的回收策略,清理操作不是實(shí)時(shí)的。緩存的維護(hù)清理通常發(fā)生在寫操作期間,如新條目的插入或現(xiàn)有條目的替換,以及在讀操作期間的偶然清理。這意味著,緩存可能會(huì)暫時(shí)超過最大容量限制和時(shí)間限制,直到下一次寫操作觸發(fā)清理。
Guava 文檔中提到,清理工作通常是在寫操作期間完成的,但是在某些情況下,讀操作也會(huì)導(dǎo)致清理,尤其是當(dāng)緩存的寫操作比較少時(shí)。這是為了確保即使在沒有寫操作的情況下,緩存也能夠維護(hù)其大小和條目的有效性。如果你需要確定緩存何時(shí)被清理,或者你想手動(dòng)控制清理操作的時(shí)機(jī)可以通過「顯式清除」的方式,條目刪除操作會(huì)立即執(zhí)行。
為了更好的理解上述說的結(jié)論,我們通過上面LoadingCache緩存的使用 結(jié)合idea debug執(zhí)行分析一下。 源碼分析見下一部分。
5、源碼分析(簡(jiǎn)短分析)
以下是guava-20.0版本的源碼分析。
- Segment中的get方法
@Override
// 1、執(zhí)行LocalLoadingCache中的get方法
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
// 2、執(zhí)行g(shù)et 或 load方法
V getOrLoad(K key) throws ExecutionException {
return get(key, defaultLoader);
}
// 3、核心get方法
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
// segmentFor方法根據(jù)hash的高位從segments數(shù)組中取出相應(yīng)的segment實(shí)例,執(zhí)行segment實(shí)例的get方法
return segmentFor(hash).get(key, hash, loader);
}
// 4、Segment中的get方法
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
// 當(dāng)前Segment中存活的條目個(gè)數(shù)不為0
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
// getEntry會(huì)校驗(yàn)key,所以key為弱引用被回收的場(chǎng)景,取到的e是null。稍后展開介紹該方法
LocalCache.ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
// 此處有個(gè)getLiveValue(),這個(gè)方法是拿到當(dāng)前存活有效的緩存值,稍后展開介紹該方法
V value = getLiveValue(e, now);
if (value != null) {
// 記錄該緩存被訪問了。此時(shí)expireAfterAccess相關(guān)的時(shí)間會(huì)被刷新
recordRead(e, now);
// 記錄緩存擊中
statsCounter.recordHits(1);
// 用來(lái)判斷是直接返回現(xiàn)有value,還是等待刷新
return scheduleRefresh(e, key, hash, value, now, loader);
}
LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
// 只有key存在,但是value不存在(被回收)、或緩存超時(shí)的情況會(huì)到達(dá)這里
// 如果已經(jīng)有線程在加載緩存了,后面的線程不會(huì)重復(fù)加載,而是等待加載的結(jié)果
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
// 如果不存在或者過期,就通過loader方法進(jìn)行加載(該方法會(huì)對(duì)當(dāng)前整個(gè)Segment加鎖,直到從數(shù)據(jù)源加載數(shù)據(jù),更新緩存);
// 走到這里的場(chǎng)景:
// 1)segment為空
// 2)key或value不存在(沒有緩存,或者弱引用、軟引用被回收),
// 3)緩存超時(shí)(expireAfterAccess或expireAfterWrite觸發(fā)的)
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw ee;
} finally {
postReadCleanup();
}
}注意事項(xiàng):
在cache get數(shù)據(jù)的時(shí)候,如果鏈表上找不到entry,或者value已經(jīng)過期,則調(diào)用lockedGetOrLoad()方法,這個(gè)方法會(huì)鎖住整個(gè)segment,直到從數(shù)據(jù)源加載數(shù)據(jù),更新緩存。
如果并發(fā)量比較大,又遇到很多key失效的情況就會(huì)很容易導(dǎo)致線程block。 項(xiàng)目實(shí)踐中需要慎重考慮這個(gè)問題,可考慮采用定時(shí)refresh機(jī)制規(guī)避該問題(下文會(huì)講述refresh機(jī)制)。
- 根據(jù)hash和key獲取鍵值對(duì):getEntry
@Nullable
ReferenceEntry<K, V> getEntry(Object key, int hash) {
// getFirst用來(lái)根據(jù)hash獲取table中相應(yīng)位置的鏈表的頭元素
for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
// hash不相等的,key肯定不相等。hash判等是int判等,比直接用key判等要快得多
if (e.getHash() != hash) {
continue;
}
K entryKey = e.getKey();
// entryKey == null的情況,是key為軟引用或者弱引用,已經(jīng)被GC回收了。直接清理掉
if (entryKey == null) {
//
tryDrainReferenceQueues();
continue;
}
if (map.keyEquivalence.equivalent(key, entryKey)) {
return e;
}
}
return null;
}- getLiveValue方法
V getLiveValue(LocalCache.ReferenceEntry<K, V> entry, long now) {
// 軟引用或者弱引用的key被清理掉了
if (entry.getKey() == null) {
// 清理非強(qiáng)引用的隊(duì)列
tryDrainReferenceQueues();
return null;
}
V value = entry.getValueReference().get();
// 軟引用的value被清理掉了
if (value == null) {
// 清理非強(qiáng)引用的隊(duì)列
tryDrainReferenceQueues();
return null;
}
// 在這里map.isExpired(entry, now)滿足條件執(zhí)行清除tryExpireEntries(now)
if (map.isExpired(entry, now)) {
tryExpireEntries(now);
return null;
}
return value;
}源碼分析部分先寫到這里。我們掌握了,基于容量、時(shí)間的回收策略,不是實(shí)時(shí)執(zhí)行的?;厥涨謇硗ǔJ窃趯懖僮髌陂g順帶進(jìn)行的,或者可以通過調(diào)用 cleanUp() 方法來(lái)顯式觸發(fā)。讀操作也可能偶爾觸發(fā)清理,尤其是在寫操作較少時(shí)。
6、刷新
了解了Guava Cache的使用和回收策略后,我們會(huì)發(fā)現(xiàn)這種用法還存在以下兩個(gè)問題:
- 緩存擊穿。數(shù)據(jù)大批量過期會(huì)導(dǎo)致對(duì)后端存儲(chǔ)的高并發(fā)訪問,加載數(shù)據(jù)過程中會(huì)鎖住整個(gè)segment,很容易導(dǎo)致線程block。
- 數(shù)據(jù)不新鮮。緩存中的數(shù)據(jù)不是最新的,特別是對(duì)于那些定期變化的數(shù)據(jù)無(wú)法做到定期刷新。
Guava Cache 的刷新機(jī)制允許緩存項(xiàng)在滿足特定條件時(shí)自動(dòng)刷新。這意味著緩存項(xiàng)的值將被重新計(jì)算和替換,但這個(gè)過程是異步的,即刷新操作不會(huì)阻塞對(duì)緩存項(xiàng)的讀取請(qǐng)求。
刷新機(jī)制主要通過 LoadingCache的refresh方法來(lái)實(shí)現(xiàn),該方法會(huì)根據(jù)緩存的 CacheLoader重新加載緩存項(xiàng)的值。通過 CacheBuilder 的 refreshAfterWrite 方法設(shè)置自動(dòng)刷新的觸發(fā)條件,即在寫入緩存項(xiàng)后的指定時(shí)間間隔。例如:
LoadingCache<KeyType, ValueType> cache = CacheBuilder.newBuilder()
// 在寫入后的10分鐘后自動(dòng)刷新
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<KeyType, ValueType>() {
@Override
public ValueType load(KeyType key) {
// 緩存項(xiàng)不存在時(shí)加載數(shù)據(jù)的方法
return loadData(key);
}
@Override
public ListenableFuture<ValueType> reload(KeyType key, ValueType oldValue) throws Exception {
// 異步刷新緩存項(xiàng)的方法
// 使用ListenableFuture來(lái)異步執(zhí)行刷新操作
return listeningExecutorService.submit(() -> loadData(key));
}
});在上述代碼中,refreshAfterWrite 設(shè)置了自動(dòng)刷新的條件,而 CacheLoader 的 reload 方法定義了如何異步刷新緩存項(xiàng)。當(dāng)緩存項(xiàng)在指定的時(shí)間間隔后被訪問時(shí),Guava Cache 會(huì)調(diào)用 reload 方法來(lái)異步加載新值。在新值加載期間,舊值仍然會(huì)返回給任何請(qǐng)求它的調(diào)用者。
需要注意的是,reload 方法應(yīng)該返回一個(gè) ListenableFuture 對(duì)象,這樣刷新操作就可以異步執(zhí)行,而不會(huì)阻塞其他緩存或線程操作。如果 reload 方法沒有被重寫,Guava Cache 將使用 load 方法進(jìn)行同步刷新。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java注解的類型知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理了一篇關(guān)于java注解的類型知識(shí)點(diǎn)總結(jié)內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-03-03
spring-boot2.7.8添加swagger的案例詳解
這篇文章主要介紹了spring-boot2.7.8添加swagger的案例詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
Spring Boot2配置Swagger2生成API接口文檔詳情
這篇文章主要介紹了Spring Boot2配置Swagger2生成API接口文檔詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
Java死鎖代碼實(shí)例及產(chǎn)生死鎖必備的四個(gè)條件
這篇文章主要介紹了Java死鎖代碼實(shí)例及產(chǎn)生死鎖必備的四個(gè)條件,Java 發(fā)生死鎖的根本原因是,在申請(qǐng)鎖時(shí)發(fā)生了交叉閉環(huán)申請(qǐng),synchronized在開發(fā)中最好不要嵌套使用,容易導(dǎo)致死鎖,需要的朋友可以參考下2024-01-01
Java 單向隊(duì)列及環(huán)形隊(duì)列的實(shí)現(xiàn)原理
本文主要介紹了Java 單向隊(duì)列及環(huán)形隊(duì)列的實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn)
本文主要介紹了MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
解決@Transactional注解事務(wù)不回滾不起作用的問題
這篇文章主要介紹了解決@Transactional注解事務(wù)不回滾不起作用的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-02-02

