摘要:緩存本次主要討論緩存。清除數據時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數據還要獲取額外的鎖,增加了消耗。
前言
Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。
我平時用的也挺頻繁,這次就借助日常使用的 Cache 組件來看看 Google 大牛們是如何設計的。
緩存本次主要討論緩存。
緩存在日常開發中舉足輕重,如果你的應用對某類數據有著較高的讀取頻次,并且改動較小時那就非常適合利用緩存來提高性能。
緩存之所以可以提高性能是因為它的讀取效率很高,就像是 CPU 的 L1、L2、L3 緩存一樣,級別越高相應的讀取速度也會越快。
但也不是什么好處都占,讀取速度快了但是它的內存更小資源更寶貴,所以我們應當緩存真正需要的數據。
其實也就是典型的空間換時間。
下面談談 Java 中所用到的緩存。
JVM 緩存首先是 JVM 緩存,也可以認為是堆緩存。
其實就是創建一些全局變量,如 Map、List 之類的容器用于存放數據。
這樣的優勢是使用簡單但是也有以下問題:
只能顯式的寫入,清除數據。
不能按照一定的規則淘汰數據,如 LRU,LFU,FIFO 等。
清除數據時的回調通知。
其他一些定制功能等。
Ehcache、Guava Cache所以出現了一些專門用作 JVM 緩存的開源工具出現了,如本文提到的 Guava Cache。
它具有上文 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() { @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
為了能看出 Guava 是怎么刪除過期數據的在獲取緩存之前休眠了 5 秒鐘,達到了超時條件。
最終會發現在 com.google.common.cache.LocalCache 類的 2187 行比較關鍵。
再跟進去之前第 2182 行會發現先要判斷 count 是否大于 0,這個 count 保存的是當前緩存的數量,并用 volatile 修飾保證了可見性。
更多關于 volatile 的相關信息可以查看 你應該知道的 volatile 關鍵字
接著往下跟到:
2761 行,根據方法名稱可以看出是判斷當前的 Entry 是否過期,該 entry 就是通過 key 查詢到的。
這里就很明顯的看出是根據根據構建時指定的過期方式來判斷當前 key 是否過期了。
如果過期就往下走,嘗試進行過期刪除(需要加鎖,后面會具體討論)。
到了這里也很清晰了:
獲取當前緩存的總數量
自減一(前面獲取了鎖,所以線程安全)
刪除并將更新的總數賦值到 count。
其實大體上就是這個流程,Guava 并沒有按照之前猜想的另起一個線程來維護過期數據。
應該是以下原因:
新起線程需要資源消耗。
維護過期數據還要獲取額外的鎖,增加了消耗。
而在查詢時候順帶做了這些事情,但是如果該緩存遲遲沒有訪問也會存在數據不能被回收的情況,不過這對于一個高吞吐的應用來說也不是問題。
總結最后再來總結下 Guava 的 Cache。
其實在上文跟代碼時會發現通過一個 key 定位數據時有以下代碼:
如果有看過 ConcurrentHashMap 的原理 應該會想到這其實非常類似。
其實 Guava Cache 為了滿足并發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這里也是一個 key 定位到一個具體位置的過程。
先找到 Segment,再找具體的位置,等于是做了兩次 Hash 定位。
上文有一個假設是對的,它內部會維護兩個隊列 accessQueue,writeQueue 用于記錄緩存順序,這樣才可以按照順序淘汰數據(類似于利用 LinkedHashMap 來做 LRU 緩存)。
同時從上文的構建方式來看,它也是構建者模式來創建對象的。
因為作為一個給開發者使用的工具,需要有很多的自定義屬性,利用構建則模式再合適不過了。
Guava 其實還有很多東西沒談到,比如它利用 GC 來回收內存,移除數據時的回調通知等。之后再接著討論。
掃碼關注微信公眾號,第一時間獲取消息。文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71187.html
摘要:緩存本次主要討論緩存。清除數據時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數據還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。 我平時用的也挺頻繁,這次就借助日...
摘要:前言在上文源碼分析原理中分析了的相關原理。我在北京模擬執行你在哪兒回復最后執行結果開始提問提問完畢,我去干其他事了收到消息你在哪兒等待響應中。。。。。回復我在北京這樣一個模擬的異步事件回調就完成了。 showImg(https://segmentfault.com/img/remote/1460000015643387?w=2048&h=1150); 前言 在上文「Guava 源碼分析...
摘要:前言在上文源碼分析原理中分析了的相關原理。我在北京模擬執行你在哪兒回復最后執行結果開始提問提問完畢,我去干其他事了收到消息你在哪兒等待響應中。。。。。回復我在北京這樣一個模擬的異步事件回調就完成了。 showImg(https://segmentfault.com/img/remote/1460000015643387?w=2048&h=1150); 前言 在上文「Guava 源碼分析...
閱讀 1391·2023-04-26 03:04
閱讀 2325·2019-08-30 15:44
閱讀 3727·2019-08-30 14:15
閱讀 3507·2019-08-27 10:56
閱讀 2703·2019-08-26 13:53
閱讀 2616·2019-08-26 13:26
閱讀 3075·2019-08-26 12:11
閱讀 3609·2019-08-23 18:21