畢竟西湖六月中,風光不與四時同。
接天蓮葉無窮碧,映日荷花別樣紅。
曉出淨慈寺送林子方-楊萬里
週末與小夥伴約了一波西湖,這個時間荷花開的正好...,在開始文章之前先放一張“佛系”美圖來鎮樓!!!
最近這段時間用了下谷歌的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的問題;我會把結論放在評論回覆裡面。
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;
}
}
}
複製程式碼