緩存的主要做用是暫時在內存中保存業務系統的數據處理結果,而且等待下次訪問使用。在日長開發有不少場合,有一些數據量不是很大,不會常常改動,而且訪問很是頻繁。可是因爲受限於硬盤IO的性能或者遠程網絡等緣由獲取可能很是的費時。會致使咱們的程序很是緩慢,這在某些業務上是不能忍的!而緩存正是解決這類問題的神器!java
固然也並非說你用了緩存你的系統就必定會變快,建議在用以前看一下使用緩存的9大誤區(上) 使用緩存的9大誤區(下)git
緩存在不少系統和架構中都用普遍的應用,例如:github
CPU緩存數據庫
操做系統緩存後端
HTTP緩存緩存
數據庫緩存服務器
靜態文件緩存網絡
本地緩存架構
分佈式緩存框架
能夠說在計算機和網絡領域,緩存是無處不在的。能夠這麼說,只要有硬件性能不對等,涉及到網絡傳輸的地方都會有緩存的身影。
緩存整體可分爲兩種 集中式緩存 和 分佈式緩存
「集中式緩存"與"分佈式緩存"的區別其實就在於「集中」與"非集中"的概念,其對象多是服務器、內存條、硬盤等。好比:
緩存集中在一臺服務器上,爲集中式緩存。
緩存分散在不一樣的服務器上,爲分佈式緩存。
緩存集中在一臺服務器的一條內存條上,爲集中式緩存。
緩存分散在一臺服務器的不一樣內存條上,爲分佈式緩存。
緩存集中在一臺服務器的一個硬盤上,爲集中式緩存。
緩存分散在一臺服務器的不一樣硬盤上,爲分佈式緩存。
想了解分佈式緩存能夠看一下淺談分佈式緩存那些事兒。
這是幾個當前比較流行的java 分佈式緩存框架5個強大的Java分佈式緩存框架推薦。
而咱們今天要講的是集中式內存緩存guava cache,這是當前咱們項目正在用的緩存工具,研究一下感受還蠻好用的。固然也有不少其餘工具,仍是看我的喜歡。oschina上面也有不少相似開源的java緩存框架
Guava Cache與ConcurrentMap很類似,但也不徹底同樣。最基本的區別是ConcurrentMap會一直保存全部添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,一般都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是頗有用的,由於它會自動加載緩存。
guava cache 加載緩存主要有兩種方式:
cacheLoader
callable callback
建立本身的CacheLoader一般只須要簡單地實現V load(K key) throws Exception
方法.
cacheLoader方式實現實例:
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder() .build( new CacheLoader<Key, Value>() { public Value load(Key key) throws AnyException { return createValue(key); } }); ... try { return cache.get(key); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
從LoadingCache查詢的正規方式是使用get(K)
方法。這個方法要麼返回已經緩存的值,要麼使用CacheLoader向緩存原子地加載新值(經過load(String key)
方法加載)。因爲CacheLoader可能拋出異常,LoadingCache.get(K)
也聲明拋出ExecutionException異常。若是你定義的CacheLoader沒有聲明任何檢查型異常,則能夠經過getUnchecked(K)
查找緩存;但必須注意,一旦CacheLoader聲明瞭檢查型異常,就不能夠調用getUnchecked(K)
。
這種方式不須要在建立的時候指定load方法,可是須要在get的時候實現一個Callable匿名內部類。
Callable方式實現實例:
Cache<Key, Value> cache = 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<Value>() { @Override public Value call() throws AnyException { return doThingsTheHardWay(key); } }); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
而若是加上如今java8裏面的Lambda表達式會看起來舒服不少
try { cache.get(key,()->{ return null; }); } catch (ExecutionException e) { e.printStackTrace(); }
全部類型的Guava Cache,無論有沒有自動加載功能,都支持get(K, Callable<V>)
方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"若是有緩存則返回;不然運算、緩存、而後返回"。
固然除了上面那種被動的加載,它還提供了主動加載的方法cache.put(key, value)
,這會直接覆蓋掉給定鍵以前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載範疇以外,因此相比於Cache.asMap().putIfAbsent(K,V)
,Cache.get(K, Callable<V>)
應該老是優先使用。
上面有提到 Guava Cache與ConcurrentMap 不同的地方在於 guava cache能夠自動回收元素,這在某種狀況下能夠更好優化資源被浪費的狀況。
當緩存設置CacheBuilder.maximumSize(size)
。這個size是指具體緩存項目的數量而不是內存的大小。並且並非說數量大於size纔會回收,而是接近size就回收。
expireAfterAccess(long, TimeUnit)
:緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收同樣。
expireAfterWrite(long, TimeUnit)
:緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回 收。若是認爲緩存數據老是在固定時候後變得陳舊不可用,這種回收方式是可取的。
cache 還提供一個Ticker方法來設置緩存失效的具體時間精度爲納秒級。
經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收:
CacheBuilder.weakKeys()
:使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
CacheBuilder.weakValues()
:使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
CacheBuilder.softValues()
:使用軟引用存儲值。軟引用只有在響應內存須要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,咱們一般建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存一樣用==而不是equals比較值。
任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收:
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除全部緩存項:Cache.invalidateAll()
這裏說一個小技巧,因爲guava cache是存在就取不存在就加載的機制,咱們能夠對緩存數據有修改的地方顯示的把它清除掉,而後再有任務去取的時候就會去數據源從新加載,這樣就能夠最大程度上保證獲取緩存的數據跟數據源是一致的。
不要被名字所迷惑,這裏指的是移除緩存的時候所觸發的監聽器。
請注意,RemovalListener拋出的任何異常都會在記錄到日誌後被丟棄[swallowed]。
LoadingCache<K , V> cache = CacheBuilder .newBuilder() .removalListener(new RemovalListener<K, V>(){ @Override public void onRemoval(RemovalNotification<K, V> notification) { System.out.println(notification.getKey()+"被移除"); } })
Lambda的寫法:
LoadingCache<K , V> cache = CacheBuilder .newBuilder() .removalListener((notification)->{ System.out.println(notification.getKey()+"已移除"); })
警告:默認狀況下,監聽器方法是在移除緩存時同步調用的。由於緩存的維護和請求響應一般是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的緩存請求。在這種狀況下,你可使用RemovalListeners.asynchronous(RemovalListener, Executor)
把監聽器裝飾爲異步操做。
這裏提一下guava cache的自動回收,並非緩存項過時起立刻清理掉,而是在讀或寫的時候作少許的維護工做,這樣作的緣由在於:若是要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操做競爭共享鎖。此外,某些環境下線程建立可能受限制,這樣CacheBuilder就不可用了。
相反,咱們把選擇權交到你手裏。若是你的緩存是高吞吐的,那就無需擔憂緩存的維護和清理等工做。若是你的緩存只會偶爾有寫操做,而你又不想清理工做阻礙了讀操做,那麼能夠建立本身的維護線程,以固定的時間間隔調用Cache.cleanUp()
。ScheduledExecutorService
能夠幫助你很好地實現這樣的定時調度。
guava cache 除了回收還提供一種刷新機制LoadingCache.refresh(K)
,他們的的區別在於,guava cache 在刷新時,其餘線程能夠繼續獲取它的舊值。這在某些狀況是很是友好的。而回收的話就必須等新值加載完成之後才能繼續讀取。並且刷新是能夠異步進行的。
若是刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。
重載CacheLoader.reload(K, V)
能夠擴展刷新時的行爲,這個方法容許開發者在計算新值時使用舊的值。
//有些鍵不須要刷新,而且咱們但願刷新是異步完成的 LoadingCache<Key, Value> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.MINUTES) .build( new CacheLoader<Key, Value>() { public Graph load(Key key) { // no checked exception return getValue(key); } public ListenableFuture<Value> reload(final Key key, Value value) { if (neverNeedsRefresh(key)) { return Futures.immediateFuture(value); } else { // asynchronous! ListenableFutureTask<Value> task = ListenableFutureTask.create(new Callable<Value>() { public Graph call() { return getValue(key); } }); executor.execute(task); return task; } } });
CacheBuilder.refreshAfterWrite(long, TimeUnit)
能夠爲緩存增長自動定時刷新功能。和expireAfterWrite
相反,refreshAfterWrite
經過定時刷新可讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新(若是CacheLoader.refresh
實現爲異步,那麼檢索不會被刷新拖慢)。所以,若是你在緩存上同時聲明expireAfterWrite
和refreshAfterWrite
,緩存並不會由於刷新盲目地定時重置,若是緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過時時間後也變得能夠回收。
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()
不會重置緩存項的讀取時間。
guava cache爲咱們實現統計功能,這在其它緩存工具裏面仍是不多有的。
CacheBuilder.recordStats()
用來開啓Guava Cache的統計功能。統計打開後, Cache.stats()
方法會返回CacheStats對象以提供以下統計信息:
hitRate()
:緩存命中率;
averageLoadPenalty()
:加載新值的平均時間,單位爲納秒;
evictionCount()
:緩存項被回收的總數,不包括顯式清除。
此外,還有其餘不少統計信息。這些統計信息對於調整緩存設置是相當重要的,在性能要求高的應用中咱們建議密切關注這些數據, 這裏咱們就不一一介紹了。
緩存雖然是個好東西,可是必定不能濫用,必定要根據本身系統的需求來妥善抉擇。
固然 guava 除了cache這塊還有不少其它很是有用的工具。
本文參考:https://github.com/google/guava/wiki/CachesExplained
做者信息
本文系力譜宿雲LeapCloud旗下MaxLeap團隊_Service&Infra成員:賈威威 【原創】
賈威威,從過後端開發已有多年,目前主要負責MaxWon服務端部分功能的開發與設計。
力譜宿雲LeapCloud 首發:https://blog.maxleap.cn/archi...