Guava 源碼分析之Cache的實現原理

Guava 源碼分析之Cache的實現原理

前言

Google 出的 Guava 是 Java 核心加強的庫,應用很是普遍。java

我平時用的也挺頻繁,此次就藉助平常使用的 Cache 組件來看看 Google 大牛們是如何設計的。git

緩存

本次主要討論緩存。緩存在平常開發中舉足輕重,若是你的應用對某類數據有着較高的讀取頻次,而且改動較小時那就很是適合利用緩存來提升性能。github

緩存之因此能夠提升性能是由於它的讀取效率很高,就像是 CPU 的 L一、L二、L3 緩存同樣,級別越高相應的讀取速度也會越快。算法

但也不是什麼好處都佔,讀取速度快了可是它的內存更小資源更寶貴,因此咱們應當緩存真正須要的數據。其實也就是典型的空間換時間。下面談談 Java 中所用到的緩存。緩存

 

JVM 緩存

首先是 JVM 緩存,也能夠認爲是堆緩存。安全

其實就是建立一些全局變量,如 Map、List 之類的容器用於存放數據。數據結構

這樣的優點是使用簡單可是也有如下問題:併發

  • 只能顯式的寫入,清除數據。
  • 不能按照必定的規則淘汰數據,如 LRU,LFU,FIFO 等。
  • 清除數據時的回調通知。
  • 其餘一些定製功能等。

Ehcache、Guava Cache

因此出現了一些專門用做 JVM 緩存的開源工具出現了,如本文提到的 Guava Cache。jvm

它具備上文 JVM 緩存不具備的功能,如自動清除數據、多種清除算法、清除回調等。分佈式

但也正由於有了這些功能,這樣的緩存必然會多出許多東西須要額外維護,天然也就增長了系統的消耗。

分佈式緩存

剛纔提到的兩種緩存其實都是堆內緩存,只能在單個節點中使用,這樣在分佈式場景下就招架不住了。

因而也有了一些緩存中間件,如 Redis、Memcached,在分佈式環境下能夠共享內存。

具體不在本次的討論範圍。

Guava Cache 示例

之因此想到 Guava 的 Cache,也是最近在作一個需求,大致以下:

從 Kafka 實時讀取出應用系統的日誌信息,該日誌信息包含了應用的健康情況。
若是在時間窗口 N 內發生了 X 次異常信息,相應的我就須要做出反饋(報警、記錄日誌等)。

對此 Guava 的 Cache 就很是適合,我利用了它的 N 個時間內不寫入數據時緩存就清空的特色,在每次讀取數據時判斷異常信息是否大於 X 便可。

僞代碼以下:

@Value("${alert.in.time:2}") private int time ; @Bean public LoadingCache buildCache(){ return CacheBuilder.newBuilder() .expireAfterWrite(time, TimeUnit.MINUTES) .build(new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long key) throws Exception { return new AtomicLong(0); } }); } /** * 判斷是否須要報警 */ public void checkAlert() { try { if (counter.get(KEY).incrementAndGet() >= limit) { LOGGER.info("***********報警***********"); //將緩存清空 counter.get(KEY).getAndSet(0L); } } catch (ExecutionException e) { LOGGER.error("Exception", e); } } 

首先是構建了 LoadingCache 對象,在 N 分鐘內不寫入數據時就回收緩存(當經過 Key 獲取不到緩存時,默認返回 0)。

而後在每次消費時候調用 checkAlert() 方法進行校驗,這樣就能夠達到上文的需求。

咱們來設想下 Guava 它是如何實現過時自動清除數據,而且是能夠按照 LRU 這樣的方式清除的。

大膽假設下:

內部經過一個隊列來維護緩存的順序,每次訪問過的數據移動到隊列頭部,而且額外開啓一個線程來判斷數據是否過時,過時就刪掉。有點相似於我以前寫過的 動手實現一個 LRU cache

胡適說過:大膽假設當心論證

下面來看看 Guava 究竟是怎麼實現。

原理分析

看原理最好不過是跟代碼一步步走了:

示例代碼在這裏:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java

8.png8.png

爲了能看出 Guava 是怎麼刪除過時數據的在獲取緩存以前休眠了 5 秒鐘,達到了超時條件。

2.png2.png

最終會發如今 com.google.common.cache.LocalCache 類的 2187 行比較關鍵。

再跟進去以前第 2182 行會發現先要判斷 count 是否大於 0,這個 count 保存的是當前緩存的數量,並用 volatile 修飾保證了可見性。

更多關於 volatile 的相關信息能夠查看 你應該知道的 volatile 關鍵字

接着往下跟到:

3.png3.png

2761 行,根據方法名稱能夠看出是判斷當前的 Entry 是否過時,該 entry 就是經過 key 查詢到的。

4.png4.png

這裏就很明顯的看出是根據根據構建時指定的過時方式來判斷當前 key 是否過時了。

5.png5.png

若是過時就往下走,嘗試進行過時刪除(須要加鎖,後面會具體討論)。

6.png6.png

到了這裏也很清晰了:

  • 獲取當前緩存的總數量
  • 自減一(前面獲取了鎖,因此線程安全)
  • 刪除並將更新的總數賦值到 count。

其實大致上就是這個流程,Guava 並無按照以前猜測的另起一個線程來維護過時數據。

應該是如下緣由:

  • 新起線程須要資源消耗。
  • 維護過時數據還要獲取額外的鎖,增長了消耗。

而在查詢時候順帶作了這些事情,可是若是該緩存遲遲沒有訪問也會存在數據不能被回收的狀況,不過這對於一個高吞吐的應用來講也不是問題。

總結

最後再來總結下 Guava 的 Cache。

其實在上文跟代碼時會發現經過一個 key 定位數據時有如下代碼:

7.png7.png

若是有看過 ConcurrentHashMap 的原理 應該會想到這其實很是相似。

其實 Guava Cache 爲了知足併發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這裏也是一個 key 定位到一個具體位置的過程。

先找到 Segment,再找具體的位置,等因而作了兩次 Hash 定位。

上文有一個假設是對的,它內部會維護兩個隊列 accessQueue,writeQueue用於記錄緩存順序,這樣才能夠按照順序淘汰數據(相似於利用 LinkedHashMap 來作 LRU 緩存)。

同時從上文的構建方式來看,它也是構建者模式來建立對象的。

由於做爲一個給開發者使用的工具,須要有不少的自定義屬性,利用構建則模式再合適不過了。

Guava 其實還有不少東西沒談到,好比它利用 GC 來回收內存,移除數據時的回調通知等。以後再接着討論。

參考:Guava 源碼分析之Cache的實現原理

相關文章
相關標籤/搜索