https://www.cnblogs.com/boothsun/p/5848143.htmlhtml
版權聲明:本文爲博主原創文章,未經博主容許不得轉載。 http://blog.csdn.net/u012859681/article/details/75220605redis
緩存在應用中是必不可少的,常常用的如redis、memcache以及內存緩存等。Guava是Google出的一個工具包,它裏面的cache便是對本地內存緩存的一種實現,支持多種緩存過時策略。
Guava cache的緩存加載方式有兩種:緩存
具體兩種方式的介紹看官方文檔:http://ifeve.com/google-guava-cachesexplained/併發
接下來看看常見的一些使用方法。
後面的示例實踐都是以CacheLoader方式加載緩存值。異步
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
如代碼所示新建了名爲caches的一個緩存對象,maximumSize定義了緩存的容量大小,當緩存數量即將到達容量上線時,則會進行緩存回收,回收最近沒有使用或整體上不多使用的緩存項。須要注意的是在接近這個容量上限時就會發生,因此在定義這個值的時候須要視狀況適量地增大一點。
另外經過expireAfterWrite這個方法定義了緩存的過時時間,寫入十分鐘以後過時。
在build方法裏,傳入了一個CacheLoader對象,重寫了其中的load方法。當獲取的緩存值不存在或已過時時,則會調用此load方法,進行緩存值的計算。
這就是最簡單也是咱們日常最經常使用的一種使用方法。定義了緩存大小、過時時間及緩存值生成方法。ide
若是用其餘的緩存方式,如redis,咱們知道上面這種「若是有緩存則返回;不然運算、緩存、而後返回」的緩存模式是有很大弊端的。當高併發條件下同時進行get操做,而此時緩存值已過時時,會致使大量線程都調用生成緩存值的方法,好比從數據庫讀取。這時候就容易形成數據庫雪崩。這也就是咱們常說的「緩存穿透」。
而Guava cache則對此種狀況有必定控制。當大量線程用相同的key獲取緩存值時,只會有一個線程進入load方法,而其餘線程則等待,直到緩存值被生成。這樣也就避免了緩存穿透的危險。高併發
如上的使用方法,雖然不會有緩存穿透的狀況,可是每當某個緩存值過時時,總是會致使大量的請求線程被阻塞。而Guava則提供了另外一種緩存策略,緩存值定時刷新:更新線程調用load方法更新該緩存,其餘請求線程返回該緩存的舊值。這樣對於某個key的緩存來講,只會有一個線程被阻塞,用來生成緩存值,而其餘的線程都返回舊的緩存值,不會被阻塞。
這裏就須要用到Guava cache的refreshAfterWrite方法。以下所示:工具
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
如代碼所示,每隔十分鐘緩存值則會被刷新。fetch
此外須要注意一個點,這裏的定時並非真正意義上的定時。Guava cache的刷新須要依靠用戶請求線程,讓該線程去進行load方法的調用,因此若是一直沒有用戶嘗試獲取該緩存值,則該緩存也並不會刷新。
如2中的使用方法,解決了同一個key的緩存過時時會讓多個線程阻塞的問題,只會讓用來執行刷新緩存操做的一個用戶線程會被阻塞。由此能夠想到另外一個問題,當緩存的key不少時,高併發條件下大量線程同時獲取不一樣key對應的緩存,此時依然會形成大量線程阻塞,而且給數據庫帶來很大壓力。這個問題的解決辦法就是將刷新緩存值的任務交給後臺線程,全部的用戶請求線程均返回舊的緩存值,這樣就不會有用戶線程被阻塞了。
詳細作法以下:
ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } @Override public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<Object>() { @Override public Object call() throws Exception { return generateValueByKey(key); } }); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
在上面的代碼中,咱們新建了一個線程池,用來執行緩存刷新任務。而且重寫了CacheLoader的reload方法,在該方法中創建緩存刷新的任務並提交到線程池。
注意此時緩存的刷新依然須要靠用戶線程來驅動,只不過和2不一樣之處在於該用戶線程觸發刷新操做以後,會立馬返回舊的緩存值。
能夠看到防緩存穿透和防用戶線程阻塞都是依靠返回舊值來完成的。因此若是沒有舊值,一樣會所有阻塞,所以應視狀況儘可能在系統啓動時將緩存內容加載到內存中。
在刷新緩存時,若是generateValueByKey方法出現異常或者返回了null,此時舊值不會更新。
題外話:在使用內存緩存時,切記拿到緩存值以後不要在業務代碼中對緩存直接作修改,由於此時拿到的對象引用是指向緩存真正的內容的。若是須要直接在該對象上進行修改,則在獲取到緩存值後拷貝一份副本,而後傳遞該副本,進行修改操做。(我曾經就犯過這個低級錯誤 - -!)
以下爲基於Guava cache抽象出來的一個緩存工具類。(抽象得很差,勉強能用 - -!)。
有改進意見麻煩多多指教。
/** * @description: 利用guava實現的內存緩存。緩存加載以後永不過時,後臺線程定時刷新緩存值。刷新失敗時將繼續返回舊緩存。 * 須要在子類中初始化refreshDuration、refreshTimeunitType、cacheMaximumSize三個參數 * 後臺刷新線程池爲該系統中全部子類共享,大小爲20. * @author: luozhuo * @date: 2017年6月21日 上午10:03:45 * @version: V1.0.0 * @param <K> * @param <V> */ public abstract class ZorroGuavaCache <K, V> { /** * 緩存自動刷新週期 */ protected int refreshDuration; /** * 緩存刷新週期時間格式 */ protected TimeUnit refreshTimeunitType; /** * 緩存最大容量 */ protected int cacheMaximumSize; private LoadingCache<K, V> cache; private ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); /** * @description: 初始化全部protected字段: * refreshDuration、refreshTimeunitType、cacheMaximumSize * @author: luozhuo * @date: 2017年6月13日 下午2:49:19 */ protected abstract void initCacheFields(); /** * @description: 定義緩存值的計算方法 * @description: 新值計算失敗時拋出異常,get操做時將繼續返回舊的緩存 * @param key * @return * @author: luozhuo * @throws Exception * @date: 2017年6月14日 下午7:11:10 */ protected abstract V getValueWhenExpire(K key) throws Exception; /** * @description: 提供給外部使用的獲取緩存方法,由實現類進行異常處理 * @param key * @return * @author: luozhuo * @date: 2017年6月15日 下午12:00:57 */ public abstract V getValue(K key); /** * @description: 獲取cache實例 * @return * @author: luozhuo * @date: 2017年6月13日 下午2:50:11 */ private LoadingCache<K, V> getCache() { if(cache == null){ synchronized (this) { if(cache == null){ initCacheFields(); cache = CacheBuilder.newBuilder() .maximumSize(cacheMaximumSize) .refreshAfterWrite(refreshDuration, refreshTimeunitType) .build(new CacheLoader<K, V>() { @Override public V load(K key) throws Exception { return getValueWhenExpire(key); } @Override public ListenableFuture<V> reload(final K key, V oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<V>() { public V call() throws Exception { return getValueWhenExpire(key); } }); } }); } } } return cache; } /** * @description: 從cache中拿出數據的操做 * @param key * @return * @throws ExecutionException * @author: luozhuo * @date: 2017年6月13日 下午5:07:11 */ protected V fetchDataFromCache(K key) throws ExecutionException { return getCache().get(key); } }