Google guava工具類的介紹和使用
https://blog.csdn.net/wwwdc1012/article/details/82228458
LoadingCache緩存使用(LoadingCache)
https://www.cnblogs.com/licunzhi/p/8818838.html
Google Guava Cache 全解析
https://www.jianshu.com/p/38bd5f1cf2f2
_________________________________________________________________
Google Guava Cache 全解析
Google Guava Cache是一種非常優秀本地緩存解決方案,提供了基於容量,時間和引用的緩存回收方式。基於容量的方式內部實現采用LRU算法,基於引用回收很好的利用了Java虛擬機的垃圾回收機制。其中的緩存構造器CacheBuilder采用構建者模式提供了設置好各種參數的緩存對象,緩存核心類LocalCache里面的內部類Segment與jdk1.7及以前的ConcurrentHashMap非常相似,都繼承於ReetrantLock,還有六個隊列,以實現豐富的本地緩存方案。
本文先介紹了Guava Cache囊括的基本使用方法,然后結合體系類圖和LocalCache的數據結構對典型的幾個方法源碼進行流程分析。
為什么要用本地緩存
相對於IO操作
速度快,效率高
相對於Redis
Redis是一種優秀的分布式緩存實現,受限於網卡等原因,遠水救不了近火。
DB + Redis + LocalCache = 高效存儲,高效訪問

什么時候用
- 願意消耗一些內存空間來提升速度
- 預料到某些鍵會被多次查詢
- 緩存中存放的數據總量不會超出內存容量
怎么用
- 設置緩存容量
- 設置超時時間
- 提供移除監聽器
- 提供緩存加載器
- 構建緩存
Demo1:
public class GuavaCacheDemo1 { public static void main(String[] args){ CacheLoader<String, String> loader = new CacheLoader<String, String> () { public String load(String key) throws Exception { Thread.sleep(1000); if("key".equals(key)) return null; System.out.println(key + " is loaded from a cacheLoader!"); return key + "'s value"; } }; RemovalListener<String, String> removalListener = new RemovalListener<String, String>() { public void onRemoval(RemovalNotification<String, String> removal) { System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!"); } }; LoadingCache<String, String> testCache = CacheBuilder.newBuilder() .maximumSize(7) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(removalListener) .build(loader); for (int i = 0; i < 10; i ++){ String key = "key" + i; String value = "value" + i; testCache.put(key,value); System.out.println("[" + key + ":" + value + "] is put into cache!"); } System.out.println(testCache.getIfPresent("key6")); try{ System.out.println(testCache.get("key")); } catch(Exception e){ e.printStackTrace(); } } }
運行效果:

加載
CacheLoader
如果有合理的默認方法來加載或計算與鍵關聯的值。
LoadingCache是附帶CacheLoader構建而成的緩存實現。創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception方法。
從LoadingCache查詢的正規方式是使用get(K)方法。這個方法要么返回已經緩存的值,要么使用CacheLoader向緩存原子地加載新值。由於CacheLoader可能拋出異常,LoadingCache.get(K)也聲明為拋出ExecutionException異常。
Callable
如果沒有合理的默認方法來加載或計算與鍵關聯的值,或者想要覆蓋默認的加載運算,同時保留“獲取緩存-如果沒有-則計算”[get-if-absent-compute]的原子語義。
所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable<V>)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有緩存則返回;否則運算、緩存、然后返回"。
Demo2:
public class GuavaCacheDemo2 { static Cache<String, String> testCache = CacheBuilder.newBuilder() .maximumSize(3) .build(); public static void main(String[] args){ testCache.put("1234","45"); System.out.println(testCache.getIfPresent("key6")); try { System.out.println(testCache.get("123", new Callable<String>() { public String call() throws Exception { return "134"; } })); System.out.println(testCache.get("1234", new Callable<String>() { public String call() throws Exception { return "134"; } })); } catch (ExecutionException e) { e.printStackTrace(); } } }
運行效果:

Cache.put
但自動加載是首選的,因為它可以更容易地推斷所有緩存內容的一致性。
使用cache.put(key, value)方法可以直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比於Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 應該總是優先使用。
緩存回收
Guava Cache提供了三種基本的緩存回收方式:
1. 基於容量回收
maximumSize(long):當緩存中的元素數量超過指定值時。
2. 定時回收
expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。
expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
如下文所討論,定時回收周期性地在寫操作中執行,偶爾在讀操作中執行。
3. 基於引用回收(Reference-based Eviction)
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。
顯式清除
任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有緩存項:Cache.invalidateAll()
移除監聽器
通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個監聽器,以便緩存項被移除時做一些額外操作。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。
統計
CacheBuilder.recordStats():用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()方法會返回CacheS tats 對象以提供如下統計信息:
hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時間,單位為納秒;
evictionCount():緩存項被回收的總數,不包括顯式清除。
此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。
Demo3:
public class GuavaCacheDemo3 { static Cache<String, Object> testCache = CacheBuilder.newBuilder() .weakValues() .recordStats() .build(); public static void main(String[] args){ Object obj1 = new Object(); testCache.put("1234",obj1); obj1 = new String("123"); System.gc(); System.out.println(testCache.getIfPresent("1234")); System.out.println(testCache.stats()); } }
運行結果

LRU緩存回收算法
LRU(Least?recently?used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那么將來被訪問的幾率也更高”。

1.?新數據插入到鏈表頭部;
2.?每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
3.?當鏈表滿的時候,將鏈表尾部的數據丟棄。
Guava Cache中借助讀寫隊列來實現LRU算法。
Guava Cache體系類圖

CacheBuilder
緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。
主要采用builder的模式,CacheBuilder的每一個方法都返回這個CacheBuilder知道build方法的調用。
注意build方法有重載,帶有參數的為構建一個具有數據加載功能的緩存,不帶參數的構建一個沒有數據加載功能的緩存。
LocalManualCache
作為LocalCache的一個內部類,在構造方法里面會把LocalCache類型的變量傳入,並且調用方法時都直接或者間接調用LocalCache里面的方法。
LocalLoadingCache
可以看到該類繼承了LocalManualCache並實現接口LoadingCache。
覆蓋了get,getUnchecked等方法。
LocalCache
Guava Cache中的核心類,重點了解。
LocalCache數據結構
根據上面的分析可知,LocalCache為Guava Cache的核心類,先看一個該類的數據結構: � LocalCache的數據結構與ConcurrentHashMap很相似,都由多個segment組成,且各segment相對獨立,互不影響,所以能支持並行操作。每個segment由一個table和若干隊列組成。緩存數據存儲在table中,其類型為AtomicReferenceArray。

Segment<K, V>[] segments;
Segment繼承於ReetrantLock,減小鎖粒度,提高並發效率。
AtomicReferenceArray<ReferenceEntry<K, V>> table;
類似於HasmMap中的table一樣,相當於entry的容器。
ReferenceEntry<K, V> referenceEntry;
基於引用的Entry,其實現類有弱引用Entry,強引用Entry等
ReferenceQueue<K> keyReferenceQueue;
已經被GC,需要內部清理的鍵引用隊列。
ReferenceQueue<V> valueReferenceQueue;
已經被GC,需要內部清理的值引用隊列。
Queue<ReferenceEntry<K, V>> recencyQueue;
記錄升級可訪問列表清單時的entries,當segment上達到臨界值或發生寫操作時該隊列會被清空。
Queue<ReferenceEntry<K, V>> writeQueue;
按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部。
Queue<ReferenceEntry<K, V>> accessQueue;
按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部。
put
public V put(K key, V value); //onlyIfAbsent為false
public V putIfAbsent(K key, V value); //onlyIfAbsent為true
該方法顯式往本地緩存里面插入值。從下面的流程圖中可以看出,在執行每次put前都會進行preWriteCleanUP,在put返回前如果更新了entry則要進行evictEntries操作。

preWriteCleanup
void preWriteCleanup(long now);
傳人參數只有當前時間。
鍵值引用隊列中都是存儲已經被GC,等待清除的entry信息,所以首先去處理這個里面的entry.
讀寫隊列里面是按照讀寫時間排序的,取出隊列中的首元素,如果當前時間與該元素的時間相差值大於設定值,則進行回收。

evictEntries
void evictEntries(ReferenceEntry<K, V> newest);
傳入的參數為最新的Entry,可能是剛插入的,也可能是剛更新過的。
該方法只有在設置了在構建緩存的時候指定了maximumSize才會往下執行。首先清除recencyQueue,判斷該元素自身的權重是否超過上限,如果超過則移除當前元素。然后判斷總的權重是否大於上限,如果超過則去accessQueue里找到隊首(即最不常訪問的元素)進行移除,直到小於上限。

getIfPresent
public V getIfPresent(Object key);
該方法從本地緩存中找值,如果找不到返回null,找到就返回相應的值。

get
首先會在緩存中找,緩存中找不到再通過load加載。

remove
public V remove(@Nullable Object key);
調用LocalManualCache的invalidate(Object key)方法即可調用remove.

maven依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.TimeUnit; public class CacheMap { private static final Logger log = LoggerFactory.getLogger(CacheMap.class); /** * @desction: 使用google guava緩存處理 */ private static Cache<String,Object> cache; static { cache = CacheBuilder.newBuilder().maximumSize(10000) .expireAfterWrite(24, TimeUnit.HOURS) .initialCapacity(10) .removalListener(new RemovalListener<String, Object>() { @Override public void onRemoval(RemovalNotification<String, Object> rn) { if(log.isInfoEnabled()){ log.info("被移除緩存{}:{}",rn.getKey(),rn.getValue()); } } }).build(); } /** * @desction: 獲取緩存 */ public static Object get(String key){ return StringUtils.isNotEmpty(key)?cache.getIfPresent(key):null; } /** * @desction: 放入緩存 */ public static void put(String key,Object value){ if(StringUtils.isNotEmpty(key) && value !=null){ cache.put(key,value); } } /** * @desction: 移除緩存 */ public static void remove(String key){ if(StringUtils.isNotEmpty(key)){ cache.invalidate(key); } } /** * @desction: 批量刪除緩存 */ public static void remove(List<String> keys){ if(keys !=null && keys.size() >0){ cache.invalidateAll(keys); } } }
______________________________________________________________________________________________________________________________________________________
Guava Cache是本地緩存的不二之選,用起來真不錯呵,可是你真的知道怎么使用才能滿足需求?今天我們深入探討一下Expire和Refresh。(廢話少說)
一、思考和猜想
首先看一下三種基於時間的清理或刷新緩存數據的方式:
expireAfterAccess: 當緩存項在指定的時間段內沒有被讀或寫就會被回收。
expireAfterWrite:當緩存項在指定的時間段內沒有更新就會被回收。
refreshAfterWrite:當緩存項上一次更新操作之后的多久會被刷新。
考慮到時效性,我們可以使用expireAfterWrite,使每次更新之后的指定時間讓緩存失效,然后重新加載緩存。guava cache會嚴格限制只有1個加載操作,這樣會很好地防止緩存失效的瞬間大量請求穿透到后端引起雪崩效應。
然而,通過分析源碼,guava cache在限制只有1個加載操作時進行加鎖,其他請求必須阻塞等待這個加載操作完成;而且,在加載完成之后,其他請求的線程會逐一獲得鎖,去判斷是否已被加載完成,每個線程必須輪流地走一個“”獲得鎖,獲得值,釋放鎖“”的過程,這樣性能會有一些損耗。這里由於我們計划本地緩存1秒,所以頻繁的過期和加載,鎖等待等過程會讓性能有較大的損耗。
因此我們考慮使用refreshAfterWrite。refreshAfterWrite的特點是,在refresh的過程中,嚴格限制只有1個重新加載操作,而其他查詢先返回舊值,這樣有效地可以減少等待和鎖爭用,所以refreshAfterWrite會比expireAfterWrite性能好。但是它也有一個缺點,因為到達指定時間后,它不能嚴格保證所有的查詢都獲取到新值。了解過guava cache的定時失效(或刷新)原來的同學都知道,guava cache並沒使用額外的線程去做定時清理和加載的功能,而是依賴於查詢請求。在查詢的時候去比對上次更新的時間,如超過指定時間則進行加載或刷新。所以,如果使用refreshAfterWrite,在吞吐量很低的情況下,如很長一段時間內沒有查詢之后,發生的查詢有可能會得到一個舊值(這個舊值可能來自於很長時間之前),這將會引發問題。