guava緩存底層實現


摘要

guava的緩存相信很多人都有用到,

Cache<String, String> cache = CacheBuilder.newBuilder()
        .expireAfterWrite(100, TimeUnit.SECONDS)
        .maximumSize(10).build();

也常用的方法是設置過期時間。但使用過程中會遇到一些問題:當過期時間到了,緩存中的對象真的會立即被釋放嗎?當緩存達到容量以后,如何高效的剔除緩存?guava cache的底層數據結構是如何的?帶着這些問題,一起來看看guava cache的源碼

介紹一下guava緩存的基本框架

image

  • LoacalCache:實現了currentMap接口,保存了一些配置信息,例如失效時間、容量等。是保存所有緩存最外層的容器
  • segment:為了高並發,借鑒了currentMap中的分段鎖機制,segment可以理解是LocalCache中的一部分,不同的segment之間並發不受影響。每次操作根據key進行hash,保證了同一個key的put和set都在同一個segment中。segment中還有兩個分別隊列用於保存軟引用或者弱引用對象回收后的引用
  • refrenceEntry:保存一個緩存key-val的對象,類似map中的entry,只不過map中entry保存的對象的直接進行,而refrenceEntry這是在中間多了一層valueReference
  • valueReference:如果是強引用,則直接保存對象的直接引用,當然也可以使用軟引用的方法。

其實通過和CurrentHashMap最類比比較好理解,只不過guava緩存在其基礎上增強了緩存過期的機制:

  1. 最大對象個數限制
  2. 超時機制
  3. 弱引用或者軟引用

guava會oom嗎

答案是肯定的,當我們設置緩存用不過期(或者很長),緩存的對象不限個數(或者很大),例如

Cache<String, String> cache = CacheBuilder.newBuilder()
        .expireAfterWrite(100000, TimeUnit.SECONDS)
        .build();

不斷向guava加入緩存大字符串,最終將能oom,解決這種辦法:

使用弱引用或者軟應用

Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .weakValues().build();

guava在創建對象放在一個map(LocalCache.class)的時候,默認使用強引用(StrongValueReference.class),如果指定使用弱引用的時候,就會創建的是(WeakValueReference.class)

合適最大容量

這個也是比較推薦的方法,根據業務需求,設置合適的緩存容量、這樣超過容量以后,緩存就會按照LRU的方式回收緩存。

CacheBuilder.maximumSize(10)

guava緩存到期就會立即清除嗎

guava清楚過期緩存的機制是什么,是單獨使用線程來掃描嗎?不是的,是在每次進行緩存操作的時候,如get()或者put()的時候,判斷緩存是否過期。核心代碼

void expireEntries(long now) {
  drainRecencyQueue(); //多線並發的情況下,防止誤刪access

  ReferenceEntry<K, V> e;
  while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
    if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
      throw new AssertionError();
    }
  }
  while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
    if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
      throw new AssertionError();
    }
  }
}

其中 writeQueue是保存按照寫入緩存先后時間的隊列,每次get或者put都可能觸發觸發這個方法。accessQueue同理,對應的是最后訪問失效時間的功能。
因此可以看出,一個如果一個對象放入緩存以后,不在有任何緩存操作(包括對緩存其他key的操作),那么該緩存不會主動過期的。不過這種情況是極端情況下才會出現。

guava如何找出最久未使用的緩存

在上面也說到了,是用accessQueue,這個隊列的實現比較復雜。這個隊列其實是按照最久未使用的順序存放的緩存對象(ReferenceEntry)的。由於會經常進行元素的移動,例如把訪問過的對象放到隊列的最后。ReferenceEntry這個在前面框架圖里面說到了,使用來保存key-val的,其中接口包含一些特殊方法:

@Override
public ReferenceEntry<K, V> getNextInAccessQueue() {
  throw new UnsupportedOperationException();
}

@Override
public void setNextInAccessQueue(ReferenceEntry<K, V> next) {
  throw new UnsupportedOperationException();
}

@Override
public ReferenceEntry<K, V> getPreviousInAccessQueue() {
  throw new UnsupportedOperationException();
}

@Override
public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) {
  throw new UnsupportedOperationException();
}

這樣通過ReferenceEntry可以就可以判斷該entry的后面節點,如果不在隊列中,則返回一個NullEntry的對象。這樣做的好處就彌補了 鏈表的缺點

  • 判斷一個ReferenceEntry是否在隊列中,只要判斷該ReferenceEntry的前一個引用是否是NullEntry,不需要便利整個鏈表

並且可以很方便的更新和刪除鏈表中的節點,因為每次訪問的時候都可能需要更新該鏈表,放入到鏈表的尾部,這樣,每次從access中拿出的頭節點就是最久未使用的。 並且,如果按照訪問時間來刪除緩存的時候,只要從隊列里找出第一個訪問沒有超時的對象,那么之前遍歷的緩存都是應該刪除的,這樣就不需要遍歷整個緩存的對象來判斷。

 

對應的writeQueue用來保存最久未更新的緩存隊列,實現方式和accessQueue一樣。

總結

可以看出,guava緩存的原型是CurrentHashMap,在其基礎上考慮如果判斷緩存是否過期。底層的一些數據結構也是用的十分巧妙。如果能仔細的看看源碼,相信對你也有一定的幫助

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM