摘要:緩存本次主要討論緩存。清除數(shù)據(jù)時(shí)的回調(diào)通知。具體不在本次的討論范圍。應(yīng)該是以下原因新起線程需要資源消耗。維護(hù)過(guò)期數(shù)據(jù)還要獲取額外的鎖,增加了消耗。
前言
Google 出的 Guava 是 Java 核心增強(qiáng)的庫(kù),應(yīng)用非常廣泛。
我平時(shí)用的也挺頻繁,這次就借助日常使用的 Cache 組件來(lái)看看 Google 大牛們是如何設(shè)計(jì)的。
緩存本次主要討論緩存。
緩存在日常開(kāi)發(fā)中舉足輕重,如果你的應(yīng)用對(duì)某類數(shù)據(jù)有著較高的讀取頻次,并且改動(dòng)較小時(shí)那就非常適合利用緩存來(lái)提高性能。
緩存之所以可以提高性能是因?yàn)樗淖x取效率很高,就像是 CPU 的 L1、L2、L3 緩存一樣,級(jí)別越高相應(yīng)的讀取速度也會(huì)越快。
但也不是什么好處都占,讀取速度快了但是它的內(nèi)存更小資源更寶貴,所以我們應(yīng)當(dāng)緩存真正需要的數(shù)據(jù)。
其實(shí)也就是典型的空間換時(shí)間。
下面談?wù)?Java 中所用到的緩存。
JVM 緩存首先是 JVM 緩存,也可以認(rèn)為是堆緩存。
其實(shí)就是創(chuàng)建一些全局變量,如 Map、List 之類的容器用于存放數(shù)據(jù)。
這樣的優(yōu)勢(shì)是使用簡(jiǎn)單但是也有以下問(wèn)題:
只能顯式的寫(xiě)入,清除數(shù)據(jù)。
不能按照一定的規(guī)則淘汰數(shù)據(jù),如 LRU,LFU,F(xiàn)IFO 等。
清除數(shù)據(jù)時(shí)的回調(diào)通知。
其他一些定制功能等。
Ehcache、Guava Cache所以出現(xiàn)了一些專門(mén)用作 JVM 緩存的開(kāi)源工具出現(xiàn)了,如本文提到的 Guava Cache。
它具有上文 JVM 緩存不具有的功能,如自動(dòng)清除數(shù)據(jù)、多種清除算法、清除回調(diào)等。
但也正因?yàn)橛辛诉@些功能,這樣的緩存必然會(huì)多出許多東西需要額外維護(hù),自然也就增加了系統(tǒng)的消耗。
分布式緩存剛才提到的兩種緩存其實(shí)都是堆內(nèi)緩存,只能在單個(gè)節(jié)點(diǎn)中使用,這樣在分布式場(chǎng)景下就招架不住了。
于是也有了一些緩存中間件,如 Redis、Memcached,在分布式環(huán)境下可以共享內(nèi)存。
具體不在本次的討論范圍。
Guava Cache 示例之所以想到 Guava 的 Cache,也是最近在做一個(gè)需求,大體如下:
從 Kafka 實(shí)時(shí)讀取出應(yīng)用系統(tǒng)的日志信息,該日志信息包含了應(yīng)用的健康狀況。
如果在時(shí)間窗口 N 內(nèi)發(fā)生了 X 次異常信息,相應(yīng)的我就需要作出反饋(報(bào)警、記錄日志等)。
對(duì)此 Guava 的 Cache 就非常適合,我利用了它的 N 個(gè)時(shí)間內(nèi)不寫(xiě)入數(shù)據(jù)時(shí)緩存就清空的特點(diǎn),在每次讀取數(shù)據(jù)時(shí)判斷異常信息是否大于 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); } }); } /** * 判斷是否需要報(bào)警 */ public void checkAlert() { try { if (counter.get(KEY).incrementAndGet() >= limit) { LOGGER.info("***********報(bào)警***********"); //將緩存清空 counter.get(KEY).getAndSet(0L); } } catch (ExecutionException e) { LOGGER.error("Exception", e); } }
首先是構(gòu)建了 LoadingCache 對(duì)象,在 N 分鐘內(nèi)不寫(xiě)入數(shù)據(jù)時(shí)就回收緩存(當(dāng)通過(guò) Key 獲取不到緩存時(shí),默認(rèn)返回 0)。
然后在每次消費(fèi)時(shí)候調(diào)用 checkAlert() 方法進(jìn)行校驗(yàn),這樣就可以達(dá)到上文的需求。
我們來(lái)設(shè)想下 Guava 它是如何實(shí)現(xiàn)過(guò)期自動(dòng)清除數(shù)據(jù),并且是可以按照 LRU 這樣的方式清除的。
大膽假設(shè)下:
內(nèi)部通過(guò)一個(gè)隊(duì)列來(lái)維護(hù)緩存的順序,每次訪問(wèn)過(guò)的數(shù)據(jù)移動(dòng)到隊(duì)列頭部,并且額外開(kāi)啟一個(gè)線程來(lái)判斷數(shù)據(jù)是否過(guò)期,過(guò)期就刪掉。有點(diǎn)類似于我之前寫(xiě)過(guò)的 動(dòng)手實(shí)現(xiàn)一個(gè) LRU cache
胡適說(shuō)過(guò):大膽假設(shè)小心論證
下面來(lái)看看 Guava 到底是怎么實(shí)現(xiàn)。
原理分析看原理最好不過(guò)是跟代碼一步步走了:
示例代碼在這里:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java
為了能看出 Guava 是怎么刪除過(guò)期數(shù)據(jù)的在獲取緩存之前休眠了 5 秒鐘,達(dá)到了超時(shí)條件。
最終會(huì)發(fā)現(xiàn)在 com.google.common.cache.LocalCache 類的 2187 行比較關(guān)鍵。
再跟進(jìn)去之前第 2182 行會(huì)發(fā)現(xiàn)先要判斷 count 是否大于 0,這個(gè) count 保存的是當(dāng)前緩存的數(shù)量,并用 volatile 修飾保證了可見(jiàn)性。
更多關(guān)于 volatile 的相關(guān)信息可以查看 你應(yīng)該知道的 volatile 關(guān)鍵字
接著往下跟到:
2761 行,根據(jù)方法名稱可以看出是判斷當(dāng)前的 Entry 是否過(guò)期,該 entry 就是通過(guò) key 查詢到的。
這里就很明顯的看出是根據(jù)根據(jù)構(gòu)建時(shí)指定的過(guò)期方式來(lái)判斷當(dāng)前 key 是否過(guò)期了。
如果過(guò)期就往下走,嘗試進(jìn)行過(guò)期刪除(需要加鎖,后面會(huì)具體討論)。
到了這里也很清晰了:
獲取當(dāng)前緩存的總數(shù)量
自減一(前面獲取了鎖,所以線程安全)
刪除并將更新的總數(shù)賦值到 count。
其實(shí)大體上就是這個(gè)流程,Guava 并沒(méi)有按照之前猜想的另起一個(gè)線程來(lái)維護(hù)過(guò)期數(shù)據(jù)。
應(yīng)該是以下原因:
新起線程需要資源消耗。
維護(hù)過(guò)期數(shù)據(jù)還要獲取額外的鎖,增加了消耗。
而在查詢時(shí)候順帶做了這些事情,但是如果該緩存遲遲沒(méi)有訪問(wèn)也會(huì)存在數(shù)據(jù)不能被回收的情況,不過(guò)這對(duì)于一個(gè)高吞吐的應(yīng)用來(lái)說(shuō)也不是問(wèn)題。
總結(jié)最后再來(lái)總結(jié)下 Guava 的 Cache。
其實(shí)在上文跟代碼時(shí)會(huì)發(fā)現(xiàn)通過(guò)一個(gè) key 定位數(shù)據(jù)時(shí)有以下代碼:
如果有看過(guò) ConcurrentHashMap 的原理 應(yīng)該會(huì)想到這其實(shí)非常類似。
其實(shí) Guava Cache 為了滿足并發(fā)場(chǎng)景的使用,核心的數(shù)據(jù)結(jié)構(gòu)就是按照 ConcurrentHashMap 來(lái)的,這里也是一個(gè) key 定位到一個(gè)具體位置的過(guò)程。
先找到 Segment,再找具體的位置,等于是做了兩次 Hash 定位。
上文有一個(gè)假設(shè)是對(duì)的,它內(nèi)部會(huì)維護(hù)兩個(gè)隊(duì)列 accessQueue,writeQueue 用于記錄緩存順序,這樣才可以按照順序淘汰數(shù)據(jù)(類似于利用 LinkedHashMap 來(lái)做 LRU 緩存)。
同時(shí)從上文的構(gòu)建方式來(lái)看,它也是構(gòu)建者模式來(lái)創(chuàng)建對(duì)象的。
因?yàn)樽鳛橐粋€(gè)給開(kāi)發(fā)者使用的工具,需要有很多的自定義屬性,利用構(gòu)建則模式再合適不過(guò)了。
Guava 其實(shí)還有很多東西沒(méi)談到,比如它利用 GC 來(lái)回收內(nèi)存,移除數(shù)據(jù)時(shí)的回調(diào)通知等。之后再接著討論。
掃碼關(guān)注微信公眾號(hào),第一時(shí)間獲取消息。文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/62007.html
摘要:緩存本次主要討論緩存。清除數(shù)據(jù)時(shí)的回調(diào)通知。具體不在本次的討論范圍。應(yīng)該是以下原因新起線程需要資源消耗。維護(hù)過(guò)期數(shù)據(jù)還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強(qiáng)的庫(kù),應(yīng)用非常廣泛。 我平時(shí)用的也挺頻繁,這次就借助日...
摘要:前言在上文源碼分析原理中分析了的相關(guān)原理。我在北京模擬執(zhí)行你在哪兒回復(fù)最后執(zhí)行結(jié)果開(kāi)始提問(wèn)提問(wèn)完畢,我去干其他事了收到消息你在哪兒等待響應(yīng)中。。。。?;貜?fù)我在北京這樣一個(gè)模擬的異步事件回調(diào)就完成了。 showImg(https://segmentfault.com/img/remote/1460000015643387?w=2048&h=1150); 前言 在上文「Guava 源碼分析...
摘要:前言在上文源碼分析原理中分析了的相關(guān)原理。我在北京模擬執(zhí)行你在哪兒回復(fù)最后執(zhí)行結(jié)果開(kāi)始提問(wèn)提問(wèn)完畢,我去干其他事了收到消息你在哪兒等待響應(yīng)中。。。。?;貜?fù)我在北京這樣一個(gè)模擬的異步事件回調(diào)就完成了。 showImg(https://segmentfault.com/img/remote/1460000015643387?w=2048&h=1150); 前言 在上文「Guava 源碼分析...
摘要:多線程編程這篇文章分析了多線程的優(yōu)缺點(diǎn),如何創(chuàng)建多線程,分享了線程安全和線程通信線程池等等一些知識(shí)。 中間件技術(shù)入門(mén)教程 中間件技術(shù)入門(mén)教程,本博客介紹了 ESB、MQ、JMS 的一些知識(shí)... SpringBoot 多數(shù)據(jù)源 SpringBoot 使用主從數(shù)據(jù)源 簡(jiǎn)易的后臺(tái)管理權(quán)限設(shè)計(jì) 從零開(kāi)始搭建自己權(quán)限管理框架 Docker 多步構(gòu)建更小的 Java 鏡像 Docker Jav...
閱讀 1682·2021-11-16 11:41
閱讀 2470·2021-11-08 13:14
閱讀 3119·2019-08-29 17:16
閱讀 3089·2019-08-29 16:30
閱讀 1856·2019-08-29 13:51
閱讀 367·2019-08-23 18:38
閱讀 3238·2019-08-23 17:14
閱讀 641·2019-08-23 15:09