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

glmapper發表於2019-03-03

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

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

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

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

週末與小夥伴約了一波西湖,這個時間荷花開的正好…,在開始文章之前先放一張“佛系”美圖來鎮樓!!!

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

一個快取定時清除任務帶來的GC問題

為什麼要從這個來說起,因為不說這個就沒guava什麼事了!

最近專案中需要使用快取來對一查查詢頻繁的資料做快取處理;首先我們也不希望引入三方的如redis或者memcache這樣的服務進來,其次是我們對於資料一致性的要求並不是很高,不需要叢集內的查詢介面共享到一份快取資料;所以這樣一來我們只要實現一個基於記憶體的快取即可。

最開始我並沒有考慮使用guava來做這個事情,而是自己寫了一套基於CurrentHashMap的快取方案;這裡需要明確一點,因為快取在這個場景裡面希望提供超時清除的能力,而基於所以在自己快取框架中增加了定時清除過期資料的能力。

這裡我就直接把定時清楚的這段程式碼放上來:

 /**
 * 靜態內部類來進行超時處理
 */
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的問題;我會把結論放在評論回覆裡面。

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

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官方教程(中文版)

相關文章