guava緩存實戰及使用場景

Guava緩存實戰及使用場景

摘要本文先介紹了爲什麼使用Guava Cache緩存,然後講解了使用方法及底層數據結構,結合實際業務,講解使用guava過程中踩過的坑,最後講解了guava可以優化的方向

1 爲什麼使用本地緩存

在多線程高併發場景中往往是離不開cache的,需要根據不同的應用場景來需要選擇不同的cache,比如分佈式緩存如Redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCache。
相比IO操作,速度快,效率高;
相比Redis,Redis是一種優秀的分佈式緩存實現,受限於網卡等原因,遠水救不了近火。
在這裏插入圖片描述

1.1 適用場景

  • 願意消耗一些內存空間來提升速度
  • 預料到某些鍵會被多次查詢
  • 緩存中存放的數據總量不會超出內存容量
  • 如果你的場景符合上述的每一條,Guava Cache就適合

2 如何使用Guava緩存(緩存容量/超時時間/移除監聽器/緩存加載器)

2.1 LoadingCache demo1

  • 導入 Maven 引用
<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>
    <version>19.0</version>  
</dependency> 
  • Cache初始化
private final LoadingCache<Long, BackCategory> backCategoryCache = CacheBuilder.newBuilder()  
        //設置cache的初始大小爲10,要合理設置該值  
        .initialCapacity(10)  
        //設置併發數爲5,即同一時間最多只能有5個線程往cache執行寫入操作  
        .concurrencyLevel(5) 
        //最大key個數
        .maximumSize(100)
        //移除監聽器
        .removalListener(removalListener)
        //設置cache中的數據在寫入之後的存活時間爲10秒  
        .expireAfterWrite(10, TimeUnit.SECONDS)  
        //構建cache實例  
        .build(new CacheLoader<Long, BackCategory>() {
              @Override
              public BackCategory load(Long categoryId) throws Exception {
                  Response<BackCategory> rBackCategory = backCategoryReadService.findById(categoryId);
                  if (!rBackCategory.isSuccess()) {
                      log.warn("failed to find back category(id={}), error code:{}",
                              categoryId, rBackCategory.getError());
                      throw new ServiceException("find back category fail,code: " + rBackCategory.getError());
                  }
                  return rBackCategory.getResult();
              }
}); 
                 
RemovalListener<String, String> removalListener = new RemovalListener<String, String>() {
    public void onRemoval(RemovalNotification<categoryId, BackCategory> removal) {
		System.out.println("[" + removal.getKey() + ":" + removal.getValue() + removal.getCause() +  "] is evicted!");
    }
};

2.2 幾個重要的組件

1、CacheBuilder 緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。採用構建者模式提供了設置好各種參數的緩存對象。
2、LocalCache數據結構。緩存核心類LocalCache數據結構與ConcurrentHashMap很相似,由多個segment組成,且各segment相對獨立,互不影響,所以能支持並行操作,每個segment由一個table和若干隊列組成。緩存數據存儲在table中,其類型爲AtomicReferenceArray。
在這裏插入圖片描述

序號 數據結構 特點
1 Segment<K, V>[] segments Segment繼承於ReetrantLock,減小鎖粒度,提高併發效率
2 AtomicReferenceArray<ReferenceEntry<K, V>> table 類似於HasmMap中的table一樣,相當於entry的容器
3 ReferenceEntry<K, V> referenceEntry 基於引用的Entry,其實現類有弱引用Entry,強引用Entry等
4 ReferenceQueue keyReferenceQueue 已經被GC,需要內部清理的鍵引用隊列
5 ReferenceQueue valueReferenceQueue 已經被GC,需要內部清理的值引用隊列
6 Queue<ReferenceEntry<K, V>> recencyQueue 記錄升級可訪問列表清單時的entries,當segment上達到臨界值或發生寫操作時該隊列會被清空
7 Queue<ReferenceEntry<K, V>> writeQueue 按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部
8 Queue<ReferenceEntry<K, V>> accessQueue 按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部

2.3 guava常用接口

/** 
 * 該接口的實現被認爲是線程安全的,即可在多線程中調用 
 * 通過被定義單例使用 
 */  
public interface Cache<K, V> {  
/** 
* 通過key獲取緩存中的value,若不存在直接返回null 
*/  
V getIfPresent(Object key);  

在這裏插入圖片描述

/** 
* 通過key獲取緩存中的value,若不存在就通過valueLoader來加載該value 
* 整個過程爲 "if cached, return; otherwise create, cache and return" 
* 注意valueLoader要麼返回非null值,要麼拋出異常,絕對不能返回null 
*/  
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; 

在這裏插入圖片描述

/** 
* 添加緩存,若key存在,就覆蓋舊值
*/  
void put(K key, V value); 

在這裏插入圖片描述

/** 
* 刪除該key關聯的緩存 
*/  
void invalidate(Object key);  

/** 
* 刪除所有緩存 
*/  
void invalidateAll();  

/** 
* 執行一些維護操作,包括清理緩存 
*/  
void cleanUp();  
}

在這裏插入圖片描述

4)緩存回收:
Guava Cache提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。 基於容量的方式內部實現採用LRU算法,基於引用回收很好的利用了Java虛擬機的垃圾回收機制。

1、基於容量的回收(size-based eviction)

如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。

2、定時回收(Timed Eviction) 常用第二種方式

CacheBuilder提供兩種定時回收的方法:
expireAfterAccess(long, TimeUnit) :緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於容量回收一樣。
expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認爲緩存數據總是在固定時候後變得陳舊不可用,這種回收方式是可取的。
定時回收週期性地在寫操作中執行,偶爾在讀操作中執行

3、基於引用的回收(Reference-based Eviction)

通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置爲允許垃圾回收
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因爲垃圾回收僅依賴恆等式(= =),使用弱引用鍵的緩存用= = 而不是equals比較鍵。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因爲垃圾回收僅依賴恆等式(= =),使用弱引用值的緩存用= =而不是equals比較值。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。

DEMO1

4、顯式清除

任何時候,你都可以顯式地清除緩存項,而不是等到它被回收
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有緩存項Cache.invalidateAll()
可以通過磐石或apollo清理guava緩存

5、移除監聽器

通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個監聽器,以便緩存項被移除時做一些額外操作。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值

DEMO2

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

  • hitRate():緩存命中率;
  • averageLoadPenalty():加載新值的平均時間,單位爲納秒;
  • evictionCount():緩存項被回收的總數,不包括顯式清除。

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

清理什麼時候發生?

使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期後馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做(如果寫操作實在太少的話)。
這樣做的原因在於:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權交到你手裏。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的 緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那麼可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度。

3 公司哪些業務中使用了guava緩存?

3.1 禪道鏈接

confluence鏈接

3.2 相關demo

1、loadingcache方法緩存了異常,那麼下一次是緩存異常還是正常數據?
deme4 : 標準中心後臺類目緩存

public class BackCategoryCacherImpl implements BackCategoryCacher {
	private final LoadingCache<Long, BackCategory> backCategoryCache;
    this.backCategoryCache = CacheBuilder.newBuilder()
          .expireAfterWrite(duration, TimeUnit.HOURS)
          .build(new CacheLoader<Long, BackCategory>() {
                @Override
                 public BackCategory load(Long categoryId) throws Exception {
                     Response<BackCategory> rBackCategory = backCategoryReadService.findById(categoryId);
                     if (!rBackCategory.isSuccess() || rBackCategory.getResult().getStatus() != 1) {
                         log.warn("failed to find back category(id={}), error code:{}",
                                 categoryId, rBackCategory.getError());
                         throw new ServiceException("find back category fail,code: " + rBackCategory.getError());
                     }
                     return rBackCategory.getResult();
                 }
             });
  @Override
 public void afterPropertiesSet() throws Exception {
      try {
          try {
              BackCategory backCategory = backCategoryCache.getUnchecked(21L);
              System.out.println("第一次查詢:" + backCategory);
          } catch (Exception e) {
          }
           BackCategory backCategory1 = backCategoryCache.getUnchecked(21L);
           System.out.println("第二次查詢:" + backCategory1);
       } catch (Exception e) {
           e.printStackTrace();
       }
}
  • 結論:第二次返回正常數據

2、guava踩坑

  • 1、使用guava cache時避免使用weakkeys,weakkeys 對key的命中規則是 ==,如果使用非基本類型,會因爲key判斷不相等導致緩存無法命中。 協議配置使用weakkeys,並且未設置緩存大熊啊,導致大量數據進入緩存,佔用內存約90%,導致項目頻繁fgc,應用響應超時;
  • 2、僅僅需緩存元數據本身,不要緩存其關係,否則造成笛卡爾積。如緩存的數據由A,B,C三張表的維度組成,緩存關係會導致ABC的數據量,如果緩存元數據,則緩存的數據量僅爲 A+B+C;
  • 3、使用緩存前必須預估緩存的數據大小,並設置緩存的數量或大小

4 優化方向

  • SpringBoot集成GuavaCache實現本地緩存
  • demo4

總結:
GuavaCache的實現代碼中沒有啓動任何線程,Cache中的所有維護操作,包括清除緩存、寫入緩存等,都需要外部調用來實現。這在需要低延遲服務場景中使用時,需要關注,可能會在某個調用的響應時間突然變大。GuavaCache畢竟是一款面向本地緩存的,輕量級的Cache,適合緩存少量數據。如果你想緩存上千萬數據,可以爲每個key設置不同的存活時間,並且高性能,那並不適合使用GuavaCache

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章