LoadingCache的使用

緩存,在我們日常開發中是必不可少的一種解決性能問題的方法。簡單的說,cache 就是爲了提升系統性能而開闢的一塊內存空間。

 緩存的主要作用是暫時在內存中保存業務系統的數據處理結果,並且等待下次訪問使用。在日常開發的很多場合,由於受限於硬盤IO的?

 緩存在很多系統和架構中都用廣泛的應用,例如:

 1.CPU緩存 
 2.操作系統緩存 
 3.本地緩存 
 4.分佈式緩存 
 5.HTTP緩存 
 6.數據庫緩存 
 等等,可以說在計算機和網絡領域,緩存無處不在。可以這麼說,只要有硬件性能不對等,涉及到網絡傳輸的地方都會有緩存的身影。


1.生成一個LoadingCache對象


  LoadingCache userCache = CacheBuilder.newBuilder()
                .maximumSize(10000))//設置緩存上線
                .expireAfterAccess(10, TimeUnit.MINUTES)//設置時間對象沒有被讀/寫訪問則對象從內存中刪除
                .expireAfterWrite(10, TimeUnit.MINUTES)//設置時間對象沒有被寫訪問則對象從內存中刪除
                //移除監聽器,緩存項被移除時會觸發
                .removalListener(new RemovalListener<String, UserProfile>() {
                    @Override
                    public void onRemoval(RemovalNotification<String, UserProfile> notification) {
                       //邏輯
                        }
                    }
                })
                .recordStats()
                //CacheLoader類 實現自動加載
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) {
                       //從SQL或者NoSql 獲取對象
                    }
                });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.CacheBuilder方法 
1) LoadingCache build(CacheLoader loader) : LoadingCache對象創建

 public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
    checkWeightWithWeigher();
    return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
  }
  • 1
  • 2
  • 3
  • 4
  • 5

2)CacheBuilder.maximumSize(long size)方法:配置緩存數量上限,快達到上限或達到上限,處理了時間最長沒被訪問過的對象或者根據配置的被釋放的對象

3)expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣

4)expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認爲緩存數據總是在固定時候後變得陳舊不可用,這種回收方式是可取的。

5)refreshAfterWrite(long duration, TimeUnit unit): 定時刷新,可以爲緩存增加自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新,即只有刷新間隔時間到了你再去get(key)纔會重新去執行Loading否則就算刷新間隔時間到了也不會執行loading操作。因此,如果你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存並不會因爲刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過期時間後也變得可以回收。還有一點比較重要的是refreshAfterWrite和expireAfterWrite兩個方法設置以後,重新get會引起loading操作都是同步串行的。這其實可能會有一個隱患,當某一個時間點剛好有大量檢索過來而且都有刷新或者回收的話,是會產生大量的請求同步調用loading方法,這些請求佔用線程資源的時間明顯變長。如正常請求也就20ms,當刷新以後加上同步請求loading這個功能接口可能響應時間遠遠大於20ms。爲了預防這種井噴現象,可以不設refreshAfterWrite方法,改用LoadingCache.refresh(K)因爲它是異步執行的,不會影響正在讀的請求,同時使用ScheduledExecutorService可以幫助你很好地實現這樣的定時調度,配上cache.asMap().keySet()返回當前所有已加載鍵,這樣所有的key定時刷新就有了。如果訪問量沒有這麼大則直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。這個可以評估自己的項目實際情況來決策。


統計相關: 
CacheBuilder.recordStats()用來開啓Guava Cache的統計功能。統計打開後,Cache.stats()方法會返回CacheStats對象以提供如下統計信息:

hitRate():緩存命中率;

averageLoadPenalty():加載新值的平均時間,單位爲納秒;

evictionCount():緩存項被回收的總數,不包括顯式清除

此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。

asMap視圖

asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:

cache.asMap()包含當前所有加載到緩存的項。因此相應地,cache.asMap().keySet()包含當前所有已加載鍵;

asMap().get(key)實質上等同於cache.getIfPresent(key),而且不會引起緩存項的加載。這和Map的語義約定一致。

所有讀寫操作都會重置相關緩存項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會重置緩存項的讀取時間。


3.LoadingCache方法的使用 
1)V get(K k): 內部調用getOrLoad(K key)方法,緩存中有對應的值則返回,沒有則使用CacheLoader load方法 
getOrLoad(K key)方法爲線程安全方法,內部加鎖 
2)V getIfPresent(Object key):緩存中有對應的值則返回,沒有則返回NULL

@Nullable
  public V getIfPresent(Object key) {
    int hash = hash(checkNotNull(key));
    V value = segmentFor(hash).get(key, hash);
    if (value == null) {
      globalStatsCounter.recordMisses(1);
    } else {
      globalStatsCounter.recordHits(1);
    }
    return value;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3)ImmutableMap getAll(Iterable keys) :提供一組keys篩選出符合條件的所有值。內部調用遍歷keys調用get(K key)方法獲得已經緩存的對象,沒有緩存的對象則通過調用CacheLoader.loadAll方法加載,如果沒實現loadAll方法則會拋出UnsupportedLoadingOperationException異常,處理這個異常最終會遍歷每個key通過lockedGetOrLoad(key, hash, loader)方法調用CacheLoader.load方法,實現加載成功

ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
    int hits = 0;
    int misses = 0;

    Map<K, V> result = Maps.newLinkedHashMap();
    Set<K> keysToLoad = Sets.newLinkedHashSet();
    for (K key : keys) {
      V value = get(key);
      if (!result.containsKey(key)) {
        result.put(key, value);
        if (value == null) {
          misses++;
          keysToLoad.add(key);
        } else {
          hits++;
        }
      }
    }

    try {
      if (!keysToLoad.isEmpty()) {
        try {
          Map<K, V> newEntries = loadAll(keysToLoad, defaultLoader);
          for (K key : keysToLoad) {
            V value = newEntries.get(key);
            if (value == null) {
              throw new InvalidCacheLoadException("loadAll failed to return a value for " + key);
            }
            result.put(key, value);
          }
        } catch (UnsupportedLoadingOperationException e) {
          // loadAll not implemented, fallback to load
          for (K key : keysToLoad) {
            misses--; // get will count this miss
            result.put(key, get(key, defaultLoader));
          }
        }
      }
      return ImmutableMap.copyOf(result);
    } finally {
      globalStatsCounter.recordHits(hits);
      globalStatsCounter.recordMisses(misses);
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

4) ImmutableMap getAll(Iterable keys): 提供一組keys篩選出符合條件緩存中存在的所有值

ImmutableMap<K, V> getAllPresent(Iterable<?> keys) {
    int hits = 0;
    int misses = 0;

    Map<K, V> result = Maps.newLinkedHashMap();
    for (Object key : keys) {
      V value = get(key);
      if (value == null) {
        misses++;
      } else {
        // TODO(fry): store entry key instead of query key
        @SuppressWarnings("unchecked")
        K castKey = (K) key;
        result.put(castKey, value);
        hits++;
      }
    }
    globalStatsCounter.recordHits(hits);
    globalStatsCounter.recordMisses(misses);
    return ImmutableMap.copyOf(result);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

5) long size() : 緩存對象數量

6)put(K key,V value): 直接顯示地向緩存中插入值,這會直接覆蓋掉已有鍵之前映射的值。

7)invalidate(Object key):顯式地清除指定key的緩存對象

public void invalidate(Object key) {
      checkNotNull(key);
      localCache.remove(key);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

8) invalidateAll(Iterable keys) : 清除批量緩存對象

public void invalidateAll(Iterable<?> keys) {
      localCache.invalidateAll(keys);
   }



void invalidateAll(Iterable<?> keys) {
    // TODO(fry): batch by segment
    for (Object key : keys) {
      remove(key);
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

9)invalidateAll(): 清除所有緩存對象

public void invalidateAll() {
      localCache.clear();
    }
  • 1
  • 2
  • 3
  • 4

10) public void refresh(K key) :刷新指定key的緩存對象,刷新和回收不太一樣。刷新表示爲鍵加載新值,這個過程可以是異步的。在刷新操作進行時,緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。重載CacheLoader.reload可以擴展刷新時的行爲,這個方法允許開發者在計算新值時使用舊的值

11)ConcurrentMap asMap():獲取緩存數據轉換成Map類型


關於guava Cache數據移除:

  guava做cache時候數據的移除方式,在guava中數據的移除分爲被動移除和主動移除兩種。 
  被動移除數據的方式,guava默認提供了三種方式: 
  1.基於大小的移除:看字面意思就知道就是按照緩存的大小來移除,如果即將到達指定的大小,那就會把不常用的鍵值對從cache中移除。 
  定義的方式一般爲 CacheBuilder.maximumSize(long),還有一種一種可以算權重的方法,個人認爲實際使用中不太用到。就這個常用的來看有幾個注意點, 
    其一,這個size指的是cache中的條目數,不是內存大小或是其他; 
    其二,並不是完全到了指定的size系統纔開始移除不常用的數據的,而是接近這個size的時候系統就會開始做移除的動作; 
    其三,如果一個鍵值對已經從緩存中被移除了,你再次請求訪問的時候,如果cachebuild是使用cacheloader方式的,那依然還是會從cacheloader中再取一次值,如果這樣還沒有,就會拋出異常 
  2.基於時間的移除:guava提供了兩個基於時間移除的方法 
    expireAfterAccess(long, TimeUnit) 這個方法是根據某個鍵值對最後一次訪問之後多少時間後移除 
    expireAfterWrite(long, TimeUnit) 這個方法是根據某個鍵值對被創建或值被替換後多少時間移除 
  3.基於引用的移除: 
  這種移除方式主要是基於java的垃圾回收機制,根據鍵或者值的引用關係決定移除 
  主動移除數據方式,主動移除有三種方法: 
  1.單獨移除用 Cache.invalidate(key) 
  2.批量移除用 Cache.invalidateAll(keys) 
  3.移除所有用 Cache.invalidateAll() 
  如果需要在移除數據的時候有所動作還可以定義Removal Listener,但是有點需要注意的是默認Removal Listener中的行爲是和移除動作同步執行的,如果需要改成異步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)

發佈了6 篇原創文章 · 獲贊 5 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章