Google Guava 在實際場景中的應用封裝

畢竟西湖六月中,風光不與四時同。java

接天蓮葉無窮碧,映日荷花別樣紅。git

曉出淨慈寺送林子方-楊萬里redis

週末與小夥伴約了一波西湖,這個時間荷花開的正好...,在開始文章以前先放一張「佛系」美圖來鎮樓!!!數據庫

最近這段時間用了下谷歌的guava,本身封了一個緩存模板方案,特此記錄,以備後續所需。緩存

一個緩存定時清除任務帶來的GC問題

爲何要從這個來講起,由於不說這個就沒guava什麼事了!數據結構

最近項目中須要使用緩存來對一查查詢頻繁的數據作緩存處理;首先咱們也不但願引入三方的如redis或者memcache這樣的服務進來,其次是咱們對於數據一致性的要求並非很高,不須要集羣內的查詢接口共享到一份緩存數據;因此這樣一來咱們只要實現一個基於內存的緩存便可。app

最開始我並無考慮使用guava來作這個事情,而是本身寫了一套基於CurrentHashMap的緩存方案;這裏須要明確一點,由於緩存在這個場景裏面但願提供超時清除的能力,而基於因此在本身緩存框架中增長了定時清除過時數據的能力。框架

這裏我就直接把定時清楚的這段代碼放上來:ide

/** * 靜態內部類來進行超時處理 */
private class ClearCacheThread extends Thread {
    @Override
    public void run() {
        while (true){
            try {
                long now = System.currentTimeMillis();
                Object[] keys = map.keySet().toArray();
                for (Object key : keys) {
                    CacheEntry entry = map.get(key);
                    if (now - entry.time >= cacheTimeout) {
                        synchronized (map) {
                            map.remove(key);
                            if (LOGGER.isDebugEnabled()){
                                LOGGER.debug("language cache timeout clear");
                            }
                        }
                    }
                }
            }catch (Exception e){
                LOGGER.error("clear out time cache value error;",e);
            }
        }
    }
}
複製代碼

這個線程是用來單獨處理過時數據的。緩存初始化時就會觸發這個線程的start方法開始執行。函數

正式因爲這段代碼的不合理致使我在發佈dev環境以後,機器GC觸發的頻次高的離譜。在嘗試了不一樣的修復方案以後,最後選擇放棄了;改用guava了!

小夥伴們能夠在下面留言來討論下這裏爲何會存在頻繁GC的問題;我會把結論放在評論回覆裏面。

guava

爲何選用guava呢,很顯然,是大佬推薦的!!!

guava是谷歌提供的一個基於內存的緩存工具包,Guava Cache 提供了一種把數據(key-value對)緩存到本地(JVM)內存中的機制,適用於不多會改動的數據。Guava Cache 與 ConcurrentMap 很類似,但也不徹底同樣。最基本的區別是 ConcurrentMap 會一直保存全部添加的元素,直到顯式地移除。相對地,Guava Cache 爲了限制內存佔用,一般都設定爲自動回收元素。

對於咱們的場景,guava 提供的能力知足了咱們的須要:

  • 數據改動小
  • 基於內存
  • 能夠自動回收

既然選擇它了,咱們仍是有必要來先對它有個大體的瞭解;先來看看它提供的一些類和接口:

接口/類 詳細解釋
Cache 【I】;定義get、put、invalidate等操做,這裏只有緩存增刪改的操做,沒有數據加載的操做。
AbstractCache 【C】;實現Cache接口。其中批量操做都是循環執行單次行爲,而單次行爲都沒有具體定義。
LoadingCache 【I】;繼承自Cache。定義get、getUnchecked、getAll等操做,這些操做都會從數據源load數據。
AbstractLoadingCache 【C】;繼承自AbstractCache,實現LoadingCache接口。
LocalCache 【C】;整個guava cache的核心類,包含了guava cache的數據結構以及基本的緩存的操做方法。
LocalManualCache 【C】;LocalCache內部靜態類,實現Cache接口。其內部的增刪改緩存操做所有調用成員變量localCache(LocalCache類型)的相應方法。
LocalLoadingCache 【C】;LocalCache內部靜態類,繼承自LocalManualCache類,實現LoadingCache接口。其全部操做也是調用成員變量localCache(LocalCache類型)的相應方法
CacheBuilder 【C】;緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。CacheBuilder在build方法中,會把前面設置的參數,所有傳遞給LocalCache,它本身實際不參與任何計算
CacheLoader 【C】;用於從數據源加載數據,定義load、reload、loadAll等操做。

整個來看的話,guava裏面最核心的應該算是 LocalCache 這個類了。

@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> 複製代碼

關於這個類的源碼這裏就不細說了,直接來看下在實際應用中個人封裝思路【封裝知足我當前的需求,若是有小夥伴須要借鑑,能夠本身在作擴展】

private static final int            MAX_SIZE     = 1000;
private static final int            EXPIRE_TIME  = 10;
private static final int            DEFAULT_SIZE = 100;

private int                         maxSize      = MAX_SIZE;
private int                         expireTime   = EXPIRE_TIME;
/** 時間單位(分鐘) */
private TimeUnit                    timeUnit     = TimeUnit.MINUTES;
/** Cache初始化或被重置的時間 */
private Date                        resetTime;

/** 分別記錄歷史最多緩存個數及時間點*/
private long                        highestSize  = 0;
private Date                        highestTime;

private volatile LoadingCache<K, V> cache;
複製代碼

這裏先是定義了一些常量和基本的屬性信息,固然這些屬性會提供set&get方法,供實際使用時去自行設置。

public LoadingCache<K, V> getCache() {
    //使用雙重校驗鎖保證只有一個cache實例
    if(cache == null){
        synchronized (this) {
            if(cache == null){
                //CacheBuilder的構造函數是私有的,只能經過其靜態方法newBuilder()來得到CacheBuilder的實例
                cache = CacheBuilder.newBuilder()
                        //設置緩存容器的初始容量爲100
                        .initialCapacity(DEFAULT_SIZE)
                        //緩存數據的最大條目
                        .maximumSize(maxSize)
                        //定時回收:緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。
                        .expireAfterWrite(expireTime, timeUnit)
                        //啓用統計->統計緩存的命中率等
                        .recordStats()
                        //設置緩存的移除通知
                        .removalListener((notification)-> {
                            if (LOGGER.isDebugEnabled()){
                                LOGGER.debug("{} was removed, cause is {}" ,notification.getKey(), notification.getCause());
                            }
                        })
                        .build(new CacheLoader<K, V>() {
                            @Override
                            public V load(K key) throws Exception {
                                return fetchData(key);
                            }
                        });
                this.resetTime = new Date();
                this.highestTime = new Date();
                if (LOGGER.isInfoEnabled()){
                    LOGGER.info("本地緩存{}初始化成功.", this.getClass().getSimpleName());
                }
            }
        }
    }

    return cache;
}
複製代碼

上面這段代碼是整個緩存的核心,經過這段代碼來生成咱們的緩存對象【使用了單例模式】。具體的屬性參數看註釋。

由於上面的那些都是封裝在一個抽象類AbstractGuavaCache裏面的,因此我又封裝了一個CacheManger用來管理緩存,並對外提供具體的功能接口;在CacheManger中,我使用了一個靜態內部類來建立當前默認的緩存。

/** * 使用靜態內部類實現一個默認的緩存,委託給manager來管理 * * DefaultGuavaCache 使用一個簡單的單例模式 * @param <String> * @param <Object> */
private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> {

    private static AbstractGuavaCache cache = new DefaultGuavaCache();

    /** * 處理自動載入緩存,按實際狀況載入 * 這裏 * @param key * @return */
    @Override
    protected Object fetchData(String key) {
        return null;
    }

    public static AbstractGuavaCache getInstance() {
        return DefaultGuavaCache.cache;
    }

}
複製代碼

大概思路就是這樣,若是須要擴展,咱們只須要按照實際的需求去擴展AbstractGuavaCache這個抽象類就能夠了。具體的代碼貼在下面了。

完整的兩個類

AbstractGuavaCache

public abstract class AbstractGuavaCache<K, V> {

    protected final Logger              LOGGER       = LoggerFactory.getLogger(AbstractGuavaCache.class);

    private static final int            MAX_SIZE     = 1000;
    private static final int            EXPIRE_TIME  = 10;
    /** 用於初始化cache的參數及其缺省值 */
    private static final int            DEFAULT_SIZE = 100;

    private int                         maxSize      = MAX_SIZE;

    private int                         expireTime   = EXPIRE_TIME;
    /** 時間單位(分鐘) */
    private TimeUnit                    timeUnit     = TimeUnit.MINUTES;
    /** Cache初始化或被重置的時間 */
    private Date                        resetTime;

    /** 分別記錄歷史最多緩存個數及時間點*/
    private long                        highestSize  = 0;
    private Date                        highestTime;

    private volatile LoadingCache<K, V> cache;

    public LoadingCache<K, V> getCache() {
        //使用雙重校驗鎖保證只有一個cache實例
        if(cache == null){
            synchronized (this) {
                if(cache == null){
                    //CacheBuilder的構造函數是私有的,只能經過其靜態方法ne
                    //wBuilder()來得到CacheBuilder的實例
                    cache = CacheBuilder.newBuilder()
                            //設置緩存容器的初始容量爲100
                            .initialCapacity(DEFAULT_SIZE)
                            //緩存數據的最大條目
                            .maximumSize(maxSize)
                            //定時回收:緩存項在給定時間內沒有被寫訪問
                            //(建立或覆蓋),則回收。
                            .expireAfterWrite(expireTime, timeUnit)
                            //啓用統計->統計緩存的命中率等
                            .recordStats()
                            //設置緩存的移除通知
                            .removalListener((notification)-> {
                                if (LOGGER.isDebugEnabled()){
                                   //...
                                }
                            })
                            .build(new CacheLoader<K, V>() {
                                @Override
                                public V load(K key) throws Exception {
                                    return fetchData(key);
                                }
                            });
                    this.resetTime = new Date();
                    this.highestTime = new Date();
                    if (LOGGER.isInfoEnabled()){
                         //...
                    }
                }
            }
        }

        return cache;
    }

    /** * 根據key從數據庫或其餘數據源中獲取一個value,並被自動保存到緩存中。 * * 改方法是模板方法,子類須要實現 * * @param key * @return value,連同key一塊兒被加載到緩存中的。 */
    protected abstract V fetchData(K key);

    /** * 從緩存中獲取數據(第一次自動調用fetchData從外部獲取數據),並處理異常 * @param key * @return Value * @throws ExecutionException */
    protected V getValue(K key) throws ExecutionException {
        V result = getCache().get(key);
        if (getCache().size() > highestSize) {
            highestSize = getCache().size();
            highestTime = new Date();
        }
        return result;
    }

    public int getMaxSize() {
        return maxSize;
    }

    public void setMaxSize(int maxSize) {
        this.maxSize = maxSize;
    }

    public int getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(int expireTime) {
        this.expireTime = expireTime;
    }

    public TimeUnit getTimeUnit() {
        return timeUnit;
    }

    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }

    public Date getResetTime() {
        return resetTime;
    }

    public void setResetTime(Date resetTime) {
        this.resetTime = resetTime;
    }

    public long getHighestSize() {
        return highestSize;
    }

    public void setHighestSize(long highestSize) {
        this.highestSize = highestSize;
    }

    public Date getHighestTime() {
        return highestTime;
    }

    public void setHighestTime(Date highestTime) {
        this.highestTime = highestTime;
    }
}
複製代碼

DefaultGuavaCacheManager

public class DefaultGuavaCacheManager {

    private static final Logger  LOGGER =
    LoggerFactory.getLogger(DefaultGuavaCacheManager.class);
   //緩存包裝類
    private static AbstractGuavaCache<String, Object> cacheWrapper;

    /** * 初始化緩存容器 */
    public static boolean initGuavaCache() {
        try {
            cacheWrapper = DefaultGuavaCache.getInstance();
            if (cacheWrapper != null) {
                return true;
            }
        } catch (Exception e) {
            LOGGER.error("Failed to init Guava cache;", e);
        }
        return false;
    }

    public static void put(String key, Object value) {
        cacheWrapper.getCache().put(key, value);
    }

    /** * 指定緩存時效 * @param key */
    public static void invalidate(String key) {
        cacheWrapper.getCache().invalidate(key);
    }

    /** * 批量清除 * @param keys */
    public static void invalidateAll(Iterable<?> keys) {
        cacheWrapper.getCache().invalidateAll(keys);
    }

    /** * 清除全部緩存項 : 慎用 */
    public static void invalidateAll() {
        cacheWrapper.getCache().invalidateAll();
    }

    public static Object get(String key) {
        try {
            return cacheWrapper.getCache().get(key);
        } catch (Exception e) {
            LOGGER.error("Failed to get value from guava cache;", e);
        }
        return null;
    }

    /** * 使用靜態內部類實現一個默認的緩存,委託給manager來管理 * * DefaultGuavaCache 使用一個簡單的單例模式 * @param <String> * @param <Object> */
    private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> {

        private static AbstractGuavaCache cache = new DefaultGuavaCache();

        /** * 處理自動載入緩存,按實際狀況載入 * @param key * @return */
        @Override
        protected Object fetchData(String key) {
            return null;
        }

        public static AbstractGuavaCache getInstance() {
            return DefaultGuavaCache.cache;
        }

    }

}
複製代碼

參考

Google Guava官方教程(中文版)

相關文章
相關標籤/搜索