Google 出的 Guava 是 Java 核心加強的庫,應用很是普遍。java
我平時用的也挺頻繁,此次就藉助平常使用的 Cache 組件來看看 Google 大牛們是如何設計的。git
本次主要討論緩存。
緩存在平常開發中舉足輕重,若是你的應用對某類數據有着較高的讀取頻次,而且改動較小時那就很是適合利用緩存來提升性能。github
緩存之因此能夠提升性能是由於它的讀取效率很高,就像是 CPU 的 L一、L二、L3
緩存同樣,級別越高相應的讀取速度也會越快。算法
但也不是什麼好處都佔,讀取速度快了可是它的內存更小資源更寶貴,因此咱們應當緩存真正須要的數據。緩存
其實也就是典型的空間換時間。
下面談談 Java 中所用到的緩存。安全
首先是 JVM 緩存,也能夠認爲是堆緩存。微信
其實就是建立一些全局變量,如 Map、List
之類的容器用於存放數據。數據結構
這樣的優點是使用簡單可是也有如下問題:併發
LRU,LFU,FIFO
等。因此出現了一些專門用做 JVM 緩存的開源工具出現了,如本文提到的 Guava Cache。分佈式
它具備上文 JVM 緩存不具備的功能,如自動清除數據、多種清除算法、清除回調等。
但也正由於有了這些功能,這樣的緩存必然會多出許多東西須要額外維護,天然也就增長了系統的消耗。
剛纔提到的兩種緩存其實都是堆內緩存,只能在單個節點中使用,這樣在分佈式場景下就招架不住了。
因而也有了一些緩存中間件,如 Redis、Memcached,在分佈式環境下能夠共享內存。
具體不在本次的討論範圍。
之因此想到 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 究竟是怎麼實現。
看原理最好不過是跟代碼一步步走了:
示例代碼在這裏:
爲了能看出 Guava 是怎麼刪除過時數據的在獲取緩存以前休眠了 5 秒鐘,達到了超時條件。
最終會發如今 com.google.common.cache.LocalCache
類的 2187 行比較關鍵。
再跟進去以前第 2182 行會發現先要判斷 count 是否大於 0,這個 count 保存的是當前緩存的數量,並用 volatile 修飾保證了可見性。
更多關於 volatile 的相關信息能夠查看 你應該知道的 volatile 關鍵字
接着往下跟到:
2761 行,根據方法名稱能夠看出是判斷當前的 Entry 是否過時,該 entry 就是經過 key 查詢到的。
這裏就很明顯的看出是根據根據構建時指定的過時方式來判斷當前 key 是否過時了。
若是過時就往下走,嘗試進行過時刪除(須要加鎖,後面會具體討論)。
到了這裏也很清晰了:
其實大致上就是這個流程,Guava 並無按照以前猜測的另起一個線程來維護過時數據。
應該是如下緣由:
而在查詢時候順帶作了這些事情,可是若是該緩存遲遲沒有訪問也會存在數據不能被回收的狀況,不過這對於一個高吞吐的應用來講也不是問題。
最後再來總結下 Guava 的 Cache。
其實在上文跟代碼時會發現經過一個 key 定位數據時有如下代碼:
若是有看過 ConcurrentHashMap 的原理 應該會想到這其實很是相似。
其實 Guava Cache 爲了知足併發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這裏也是一個 key 定位到一個具體位置的過程。
先找到 Segment,再找具體的位置,等因而作了兩次 Hash 定位。
上文有一個假設是對的,它內部會維護兩個隊列 accessQueue,writeQueue
用於記錄緩存順序,這樣才能夠按照順序淘汰數據(相似於利用 LinkedHashMap 來作 LRU 緩存)。
同時從上文的構建方式來看,它也是構建者模式來建立對象的。
由於做爲一個給開發者使用的工具,須要有不少的自定義屬性,利用構建則模式再合適不過了。
Guava 其實還有不少東西沒談到,好比它利用 GC 來回收內存,移除數據時的回調通知等。以後再接着討論。