欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用Guava?Cache原理及最佳實(shí)踐

 更新時(shí)間:2025年02月07日 10:23:02   作者:GNG  
文章介紹了GuavaCache,這是一種支持高并發(fā)的本地緩存,它支持多種回收策略,如基于容量、時(shí)間和引用的回收,并提供了自動(dòng)加載、定時(shí)刷新和顯式清除緩存的功能,文章還討論了GuavaCache的適用場(chǎng)景和使用方法,并通過代碼示例和源碼分析,幫助讀者更好地理解和使用GuavaCache

緩存的種類有很多,需要根據(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被封裝在WeakReferenceSoftReference引用內(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í)主要分二種模式:LoadingCacheCallableCache

核心區(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í)間的回收策略。

  1. 基于寫操作的回收(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();
  1. 基于訪問操作的回收(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)回收這些緩存條目。

  1. 弱引用鍵(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í),可以安全地回收。

  1. 軟引用值(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è)或某些條目。

  1. 移除單個(gè)條目

使用 invalidate(key) 方法可以移除緩存中的特定鍵對(duì)應(yīng)的條目。

cache.invalidate(key);
  1. 移除多個(gè)條目

使用 invalidateAll(keys) 方法可以移除緩存中所有在給定集合中的鍵對(duì)應(yīng)的條目。

cache.invalidateAll(keys);
  1. 移除所有條目

使用 invalidateAll() 方法可以移除緩存中的所有條目。

cache.invalidateAll();
  1. 使用 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í)間。

  1. 注冊(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版本的源碼分析。

  1. 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ī)制)。

  1. 根據(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;
    }
  1. 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è)問題:

  1. 緩存擊穿。數(shù)據(jù)大批量過期會(huì)導(dǎo)致對(duì)后端存儲(chǔ)的高并發(fā)訪問,加載數(shù)據(jù)過程中會(huì)鎖住整個(gè)segment,很容易導(dǎo)致線程block。
  2. 數(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é)

    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的案例詳解

    這篇文章主要介紹了spring-boot2.7.8添加swagger的案例詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2024-01-01
  • Spring Boot2配置Swagger2生成API接口文檔詳情

    Spring Boot2配置Swagger2生成API接口文檔詳情

    這篇文章主要介紹了Spring Boot2配置Swagger2生成API接口文檔詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • SpringBoot整合RocketMQ的詳細(xì)過程

    SpringBoot整合RocketMQ的詳細(xì)過程

    這篇文章主要介紹了SpringBoot整合RocketMQ的詳細(xì)過程,本文分為三部分,第一部分實(shí)現(xiàn)SpringBoot與RocketMQ的整合,第二部分解決在使用RocketMQ過程中可能遇到的一些問題并解決他們,第三部分介紹如何封裝RocketMQ以便更好地使用,需要的朋友可以參考下
    2023-04-04
  • SpringBoot bean查詢加載順序流程詳解

    SpringBoot bean查詢加載順序流程詳解

    當(dāng)你在項(xiàng)目啟動(dòng)時(shí)需要提前做一個(gè)業(yè)務(wù)的初始化工作時(shí),或者你正在開發(fā)某個(gè)中間件需要完成自動(dòng)裝配時(shí)。你會(huì)聲明自己的Configuration類,但是可能你面對(duì)的是好幾個(gè)有互相依賴的Bean
    2023-03-03
  • Java死鎖代碼實(shí)例及產(chǎn)生死鎖必備的四個(gè)條件

    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)原理

    本文主要介紹了Java 單向隊(duì)列及環(huán)形隊(duì)列的實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-10-10
  • SpringBoot整合Swagger2代碼實(shí)例

    SpringBoot整合Swagger2代碼實(shí)例

    這篇文章主要介紹了SpringBoot整合Swagger2代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn)

    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à)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • 解決@Transactional注解事務(wù)不回滾不起作用的問題

    解決@Transactional注解事務(wù)不回滾不起作用的問題

    這篇文章主要介紹了解決@Transactional注解事務(wù)不回滾不起作用的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02

最新評(píng)論