摘要:緩存總體可分為兩種集中式緩存和分布式緩存集中式緩存與分布式緩存的區(qū)別其實就在于集中與非集中的概念,其對象可能是服務器內存條硬盤等。內存條版本緩存集中在一臺服務器的一條內存條上,為集中式緩存。
背景
緩存的主要作用是暫時在內存中保存業(yè)務系統(tǒng)的數(shù)據(jù)處理結果,并且等待下次訪問使用。在日長開發(fā)有很多場合,有一些數(shù)據(jù)量不是很大,不會經常改動,并且訪問非常頻繁。但是由于受限于硬盤IO的性能或者遠程網絡等原因獲取可能非常的費時。會導致我們的程序非常緩慢,這在某些業(yè)務上是不能忍的!而緩存正是解決這類問題的神器!
當然也并不是說你用了緩存你的系統(tǒng)就一定會變快,建議在用之前看一下使用緩存的9大誤區(qū)(上) 使用緩存的9大誤區(qū)(下)
緩存在很多系統(tǒng)和架構中都用廣泛的應用,例如:
CPU緩存
操作系統(tǒng)緩存
HTTP緩存
數(shù)據(jù)庫緩存
靜態(tài)文件緩存
本地緩存
分布式緩存
可以說在計算機和網絡領域,緩存是無處不在的。可以這么說,只要有硬件性能不對等,涉及到網絡傳輸?shù)牡胤蕉紩芯彺娴纳碛啊?
緩存總體可分為兩種 集中式緩存 和 分布式緩存
“集中式緩存"與"分布式緩存"的區(qū)別其實就在于“集中”與"非集中"的概念,其對象可能是服務器、內存條、硬盤等。比如:
緩存集中在一臺服務器上,為集中式緩存。
緩存分散在不同的服務器上,為分布式緩存。
緩存集中在一臺服務器的一條內存條上,為集中式緩存。
緩存分散在一臺服務器的不同內存條上,為分布式緩存。
緩存集中在一臺服務器的一個硬盤上,為集中式緩存。
緩存分散在一臺服務器的不同硬盤上,為分布式緩存。
想了解分布式緩存可以看一下淺談分布式緩存那些事兒。
這是幾個當前比較流行的java 分布式緩存框架5個強大的Java分布式緩存框架推薦。
而我們今天要講的是集中式內存緩存guava cache,這是當前我們項目正在用的緩存工具,研究一下感覺還蠻好用的。當然也有很多其他工具,還是看個人喜歡。oschina上面也有很多類似開源的java緩存框架
正文Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區(qū)別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache為了限制內存占用,通常都設定為自動回收元素。在某些場景下,盡管LoadingCache 不回收元素,它也是很有用的,因為它會自動加載緩存。
guava cache 加載緩存主要有兩種方式:
cacheLoader
callable callback
cacheLoader創(chuàng)建自己的CacheLoader通常只需要簡單地實現(xiàn)V load(K key) throws Exception方法.
cacheLoader方式實現(xiàn)實例:
LoadingCachecache = CacheBuilder.newBuilder() .build( new CacheLoader () { public Value load(Key key) throws AnyException { return createValue(key); } }); ... try { return cache.get(key); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
從LoadingCache查詢的正規(guī)方式是使用get(K)方法。這個方法要么返回已經緩存的值,要么使用CacheLoader向緩存原子地加載新值(通過load(String key) 方法加載)。由于CacheLoader可能拋出異常,LoadingCache.get(K)也聲明拋出ExecutionException異常。如果你定義的CacheLoader沒有聲明任何檢查型異常,則可以通過getUnchecked(K)查找緩存;但必須注意,一旦CacheLoader聲明了檢查型異常,就不可以調用getUnchecked(K)。
Callable這種方式不需要在創(chuàng)建的時候指定load方法,但是需要在get的時候實現(xiàn)一個Callable匿名內部類。
Callable方式實現(xiàn)實例:
Cachecache = CacheBuilder.newBuilder() .build(); // look Ma, no CacheLoader ... try { // If the key wasn"t in the "easy to compute" group, we need to // do things the hard way. cache.get(key, new Callable () { @Override public Value call() throws AnyException { return doThingsTheHardWay(key); } }); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
而如果加上現(xiàn)在java8里面的Lambda表達式會看起來舒服很多
try { cache.get(key,()->{ return null; }); } catch (ExecutionException e) { e.printStackTrace(); }
所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable
當然除了上面那種被動的加載,它還提供了主動加載的方法cache.put(key, value),這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable
上面有提到 Guava Cache與ConcurrentMap 不一樣的地方在于 guava cache可以自動回收元素,這在某種情況下可以更好優(yōu)化資源被浪費的情況。
基于容量的回收當緩存設置CacheBuilder.maximumSize(size)。這個size是指具體緩存項目的數(shù)量而不是內存的大小。而且并不是說數(shù)量大于size才會回收,而是接近size就回收。
定時回收
expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基于大小回收一樣。
expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創(chuàng)建或覆蓋),則回 收。如果認為緩存數(shù)據(jù)總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
cache 還提供一個Ticker方法來設置緩存失效的具體時間精度為納秒級。
基于引用的回收通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式(==),使用弱引用值的緩存用==而不是equals比較值。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基于容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。
顯式清除任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有緩存項:Cache.invalidateAll()
這里說一個小技巧,由于guava cache是存在就取不存在就加載的機制,我們可以對緩存數(shù)據(jù)有修改的地方顯示的把它清除掉,然后再有任務去取的時候就會去數(shù)據(jù)源重新加載,這樣就可以最大程度上保證獲取緩存的數(shù)據(jù)跟數(shù)據(jù)源是一致的。
移除監(jiān)聽器不要被名字所迷惑,這里指的是移除緩存的時候所觸發(fā)的監(jiān)聽器。
請注意,RemovalListener拋出的任何異常都會在記錄到日志后被丟棄[swallowed]。
LoadingCachecache = CacheBuilder .newBuilder() .removalListener(new RemovalListener (){ @Override public void onRemoval(RemovalNotification notification) { System.out.println(notification.getKey()+"被移除"); } })
Lambda的寫法:
LoadingCachecache = CacheBuilder .newBuilder() .removalListener((notification)->{ System.out.println(notification.getKey()+"已移除"); })
警告:默認情況下,監(jiān)聽器方法是在移除緩存時同步調用的。因為緩存的維護和請求響應通常是同時進行的,代價高昂的監(jiān)聽器方法在同步模式下會拖慢正常的緩存請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽器裝飾為異步操作。
這里提一下guava cache的自動回收,并不是緩存項過期起馬上清理掉,而是在讀或寫的時候做少量的維護工作,這樣做的原因在于:如果要自動地持續(xù)清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環(huán)境下線程創(chuàng)建可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權交到你手里。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創(chuàng)建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現(xiàn)這樣的定時調度。
刷新guava cache 除了回收還提供一種刷新機制LoadingCache.refresh(K),他們的的區(qū)別在于,guava cache 在刷新時,其他線程可以繼續(xù)獲取它的舊值。這在某些情況是非常友好的。而回收的話就必須等新值加載完成以后才能繼續(xù)讀取。而且刷新是可以異步進行的。
如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄[swallowed]。
重載CacheLoader.reload(K, V)可以擴展刷新時的行為,這個方法允許開發(fā)者在計算新值時使用舊的值。
//有些鍵不需要刷新,并且我們希望刷新是異步完成的 LoadingCachegraphs = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.MINUTES) .build( new CacheLoader () { public Graph load(Key key) { // no checked exception return getValue(key); } public ListenableFuture reload(final Key key, Value value) { if (neverNeedsRefresh(key)) { return Futures.immediateFuture(value); } else { // asynchronous! ListenableFutureTask task = ListenableFutureTask.create(new Callable () { public Graph call() { return getValue(key); } }); executor.execute(task); return task; } } });
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以為緩存增加自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時才會真正刷新(如果CacheLoader.refresh實現(xiàn)為異步,那么檢索不會被刷新拖慢)。因此,如果你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存并不會因為刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發(fā)生,緩存項在過期時間后也變得可以回收。
asMap視圖asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:
cache.asMap()包含當前所有加載到緩存的項。因此相應地,cache.asMap().keySet()包含當前所有已加載鍵;
asMap().get(key)實質上等同于cache.getIfPresent(key),而且不會引起緩存項的加載。這和Map的語義約定一致。
所有讀寫操作都會重置相關緩存項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會重置緩存項的讀取時間。
統(tǒng)計guava cache為我們實現(xiàn)統(tǒng)計功能,這在其它緩存工具里面還是很少有的。
CacheBuilder.recordStats()用來開啟Guava Cache的統(tǒng)計功能。統(tǒng)計打開后, Cache.stats()方法會返回CacheStats對象以提供如下統(tǒng)計信息:
hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時間,單位為納秒;
evictionCount():緩存項被回收的總數(shù),不包括顯式清除。
此外,還有其他很多統(tǒng)計信息。這些統(tǒng)計信息對于調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數(shù)據(jù), 這里我們就不一一介紹了。
緩存雖然是個好東西,但是一定不能濫用,一定要根據(jù)自己系統(tǒng)的需求來妥善抉擇。
當然 guava 除了cache這塊還有很多其它非常有用的工具。
本文參考:https://github.com/google/guava/wiki/CachesExplained
作者信息
本文系力譜宿云LeapCloud旗下MaxLeap團隊_Service&Infra成員:賈威威 【原創(chuàng)】
賈威威,從事后端開發(fā)已有多年,目前主要負責MaxWon服務端部分功能的開發(fā)與設計。
力譜宿云LeapCloud 首發(fā):https://blog.maxleap.cn/archi...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/61809.html
摘要:緩存總體可分為兩種集中式緩存和分布式緩存集中式緩存與分布式緩存的區(qū)別其實就在于集中與非集中的概念,其對象可能是服務器內存條硬盤等。內存條版本緩存集中在一臺服務器的一條內存條上,為集中式緩存。 背景 緩存的主要作用是暫時在內存中保存業(yè)務系統(tǒng)的數(shù)據(jù)處理結果,并且等待下次訪問使用。在日長開發(fā)有很多場合,有一些數(shù)據(jù)量不是很大,不會經常改動,并且訪問非常頻繁。但是由于受限于硬盤IO的性能或者遠程...
摘要:最基本的區(qū)別是會一直保存所有添加的元素,直到顯式地移除。相對地,為了限制內存占用,通常都設定為自動回收元素。消息接收方消息發(fā)起方同步異步注冊事件觸發(fā)事件處理這個錯誤可能是由于中對應方法拋出了異常。 緩存 Guava Cache提供了內存緩存功能。內存緩存需要考慮很多問題,包括并發(fā)問題,緩存失效機制,內存不夠用時緩存釋放,緩存的命中率,緩存的移除等等。 當然這些東西Guava都考慮到了。...
摘要:緩存本次主要討論緩存。清除數(shù)據(jù)時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數(shù)據(jù)還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。 我平時用的也挺頻繁,這次就借助日...
摘要:緩存本次主要討論緩存。清除數(shù)據(jù)時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數(shù)據(jù)還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。 我平時用的也挺頻繁,這次就借助日...
閱讀 540·2021-08-31 09:45
閱讀 1647·2021-08-11 11:19
閱讀 883·2019-08-30 15:55
閱讀 821·2019-08-30 10:52
閱讀 2845·2019-08-29 13:11
閱讀 2924·2019-08-23 17:08
閱讀 2832·2019-08-23 15:11
閱讀 3066·2019-08-23 14:33