Guava Cache內存緩存使用實踐-定時異步刷新及簡單抽象封裝

https://www.cnblogs.com/boothsun/p/5848143.htmlhtml

 

版權聲明:本文爲博主原創文章,未經博主容許不得轉載。 http://blog.csdn.net/u012859681/article/details/75220605redis

目錄(?)[-]數據庫

  1. 簡單使用定時過時
  2. 進階使用定時刷新
  3. 進階使用異步刷新
  4. TIPS
  5. 簡單抽象封裝

緩存在應用中是必不可少的,常常用的如redis、memcache以及內存緩存等。Guava是Google出的一個工具包,它裏面的cache便是對本地內存緩存的一種實現,支持多種緩存過時策略。 
Guava cache的緩存加載方式有兩種:緩存

  • CacheLoader
  • Callable callback

具體兩種方式的介紹看官方文檔:http://ifeve.com/google-guava-cachesexplained/併發

接下來看看常見的一些使用方法。 
後面的示例實踐都是以CacheLoader方式加載緩存值。異步

1.簡單使用:定時過時

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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如代碼所示新建了名爲caches的一個緩存對象,maximumSize定義了緩存的容量大小,當緩存數量即將到達容量上線時,則會進行緩存回收,回收最近沒有使用或整體上不多使用的緩存項。須要注意的是在接近這個容量上限時就會發生,因此在定義這個值的時候須要視狀況適量地增大一點。 
另外經過expireAfterWrite這個方法定義了緩存的過時時間,寫入十分鐘以後過時。 
在build方法裏,傳入了一個CacheLoader對象,重寫了其中的load方法。當獲取的緩存值不存在或已過時時,則會調用此load方法,進行緩存值的計算。 
這就是最簡單也是咱們日常最經常使用的一種使用方法。定義了緩存大小、過時時間及緩存值生成方法。ide

若是用其餘的緩存方式,如redis,咱們知道上面這種「若是有緩存則返回;不然運算、緩存、而後返回」的緩存模式是有很大弊端的。當高併發條件下同時進行get操做,而此時緩存值已過時時,會致使大量線程都調用生成緩存值的方法,好比從數據庫讀取。這時候就容易形成數據庫雪崩。這也就是咱們常說的「緩存穿透」。 
而Guava cache則對此種狀況有必定控制。當大量線程用相同的key獲取緩存值時,只會有一個線程進入load方法,而其餘線程則等待,直到緩存值被生成。這樣也就避免了緩存穿透的危險。高併發

2.進階使用:定時刷新

如上的使用方法,雖然不會有緩存穿透的狀況,可是每當某個緩存值過時時,總是會致使大量的請求線程被阻塞。而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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如代碼所示,每隔十分鐘緩存值則會被刷新。fetch

此外須要注意一個點,這裏的定時並非真正意義上的定時。Guava cache的刷新須要依靠用戶請求線程,讓該線程去進行load方法的調用,因此若是一直沒有用戶嘗試獲取該緩存值,則該緩存也並不會刷新。

3.進階使用:異步刷新

如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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在上面的代碼中,咱們新建了一個線程池,用來執行緩存刷新任務。而且重寫了CacheLoader的reload方法,在該方法中創建緩存刷新的任務並提交到線程池。 
注意此時緩存的刷新依然須要靠用戶線程來驅動,只不過和2不一樣之處在於該用戶線程觸發刷新操做以後,會立馬返回舊的緩存值。

TIPS

  • 能夠看到防緩存穿透和防用戶線程阻塞都是依靠返回舊值來完成的。因此若是沒有舊值,一樣會所有阻塞,所以應視狀況儘可能在系統啓動時將緩存內容加載到內存中。

  • 在刷新緩存時,若是generateValueByKey方法出現異常或者返回了null,此時舊值不會更新。

  • 題外話:在使用內存緩存時,切記拿到緩存值以後不要在業務代碼中對緩存直接作修改,由於此時拿到的對象引用是指向緩存真正的內容的。若是須要直接在該對象上進行修改,則在獲取到緩存值後拷貝一份副本,而後傳遞該副本,進行修改操做。(我曾經就犯過這個低級錯誤 - -!)

4.簡單抽象封裝

以下爲基於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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
相關文章
相關標籤/搜索