使用Guava?Cache原理及最佳實(shí)踐
緩存的種類有很多,需要根據(jù)不同的應(yīng)用場(chǎng)景來選擇不同的cache,比如分布式緩存如redis、memcached,還有本地(進(jìn)程內(nèi))緩存如:ehcache、GuavaCache、Caffeine。
本篇主要圍繞全內(nèi)存緩存-Guava Cache做一些詳細(xì)的講解和分析。
1. Guava Cache是什么
1.1 簡介
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說簡單點(diǎn)就是一個(gè)支持LRU的ConcurrentHashMap,并提供了基于容量,時(shí)間和引用的緩存回收方式。(簡單概括)
1.3 適用場(chǎng)景
- 愿意消耗一些內(nè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)方法來加載或計(jì)算與鍵關(guān)聯(lián)的值,CallableCache創(chuàng)建時(shí)無需關(guān)聯(lián)固定的CacheLoader使用起來更加靈活。
前置準(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ù)庫中取數(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來構(gòu)建LoadingCache實(shí)例,可以鏈?zhǔn)秸{(diào)用多個(gè)方法來配置緩存的行為。其中CacheLoader可以理解為一個(gè)固定的加載器,在創(chuàng)建LoadingCache時(shí)指定,然后簡單地重寫V load(K key) throws Exception方法,就可以達(dá)到當(dāng)檢索不存在的時(shí)候自動(dòng)加載數(shù)據(jù)的效果。
//創(chuàng)建一個(gè)LoadingCache,并可以進(jì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)行一些簡單的緩存配置 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)該沒啥問題了,接下來一起學(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
方法來設(shè)置緩存的最大條目數(shù)。
當(dāng)緩存中的條目數(shù)量超過了最大值時(shí),Guava Cache 會(huì)根據(jù)LRU(最近最少使用)
算法來移除一些條目。例如:
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder() // 緩存最多可以存儲(chǔ)1000個(gè)條目 .maximumSize(1000) .build();
除了 maximumSize,Guava Cache 還提供了 maximumWeight
方法和 weigher
方法,允許你根據(jù)每個(gè)條目的權(quá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ū)懭耄瑒t會(huì)被回收。這種策略適用于需要回收那些可能很長時(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)來存儲(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 接口提供的方法來移除條目。
// 移除單個(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 的語義約定一致。
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、源碼分析(簡短分析)
以下是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); // 用來判斷是直接返回現(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用來根據(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() 方法來顯式觸發(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ù)無法做到定期刷新。
Guava Cache 的刷新機(jī)制允許緩存項(xiàng)在滿足特定條件時(shí)自動(dòng)刷新。這意味著緩存項(xiàng)的值將被重新計(jì)算和替換,但這個(gè)過程是異步的,即刷新操作不會(huì)阻塞對(duì)緩存項(xiàng)的讀取請(qǐng)求。
刷新機(jī)制主要通過 LoadingCache的refresh方法來實(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來異步執(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 方法來異步加載新值。在新值加載期間,舊值仍然會(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-03spring-boot2.7.8添加swagger的案例詳解
這篇文章主要介紹了spring-boot2.7.8添加swagger的案例詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01Spring Boot2配置Swagger2生成API接口文檔詳情
這篇文章主要介紹了Spring Boot2配置Swagger2生成API接口文檔詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09Java死鎖代碼實(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-01Java 單向隊(duì)列及環(huán)形隊(duì)列的實(shí)現(xiàn)原理
本文主要介紹了Java 單向隊(duì)列及環(huán)形隊(duì)列的實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn)
本文主要介紹了MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07解決@Transactional注解事務(wù)不回滾不起作用的問題
這篇文章主要介紹了解決@Transactional注解事務(wù)不回滾不起作用的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02