簡介
Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,通常都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是很有用的,因爲它會自動加載緩存。
通常來說,Guava Cache適用於:
- 你願意消耗一些內存空間來提升速度。
- 你預料到某些鍵會被查詢一次以上。
- 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。如果這不符合你的需求,請嘗試Memcached這類工具)
如果你的場景符合上述的每一條,Guava Cache就適合你。
如同範例代碼展示的一樣,Cache實例通過CacheBuilder生成器模式獲取,但是自定義你的緩存纔是最有趣的部分。
注:如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的內存效率——但Cache的大多數特性都很難基於舊有的ConcurrentMap複製,甚至根本不可能做到。
Guava Cache的使用示例
使用緩存時,最常遇到的場景需要就是:
"獲取緩存-如果沒有-則計算"[get-if-absent-compute]的原子語義.
具體含義:
- 從緩存中取。
- 緩存中存在該數據,直接返回;
- 緩存中不存在該數據,從數據源中取。
- 數據源中存在該數據,放入緩存,並返回;
- 數據源中不存在該數據,返回空。
刷新機制
三種基於時間的清理或刷新緩存數據的方式:
expireAfterAccess: 當緩存項在指定的時間段內沒有被讀或寫就會被回收。
expireAfterWrite:當緩存項在指定的時間段內沒有更新就會被回收。
refreshAfterWrite:當緩存項上一次更新操作之後的多久會被刷新。
一、定時過期
LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(30, 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方法,進行緩存值的計算。
二、定時刷新
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();
}
每隔十分鐘緩存值則會被刷新。
有一個需要注意也很難處理的地方,這裏的定時並不是真正意義上的定時。Guava cache的刷新需要依靠用戶請求線程,讓該線程去進行load方法的調用,所以如果一直沒有用戶嘗試獲取該緩存值,則該緩存也並不會刷新。
三、異步刷新
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();
}
當緩存的key很多時,高併發條件下大量線程同時獲取不同key對應的緩存,此時依然會造成大量線程阻塞,並且給數據庫帶來很大壓力。這個問題的解決辦法就是將刷新緩存值的任務交給後臺線程,所有的用戶請求線程均返回舊的緩存值,這樣就不會有用戶線程被阻塞了。
{
-
可以看到防緩存穿透和防用戶線程阻塞都是依靠返回舊值來完成的。所以如果沒有舊值,同樣會全部阻塞,因此應視情況儘量在系統啓動時將緩存內容加載到內存中。
-
在刷新緩存時,如果generateValueByKey方法出現異常或者返回了null,此時舊值不會更新。
-
題外話:在使用內存緩存時,切記拿到緩存值之後不要在業務代碼中對緩存直接做修改,因爲此時拿到的對象引用是指向緩存真正的內容的。如果需要直接在該對象上進行修改,則在獲取到緩存值後拷貝一份副本,然後傳遞該副本,進行修改操作
}
封裝
封裝後的的緩存抽象類實現了刷新時間、時間單位、定時刷新及初始化緩存值等方法。實現類只需要設置自己的緩存時間等信息,實現preload()方法加載緩存數據,實現getCacheValue()方法將緩存數據添加到緩存中。
抽象類代碼如下:
package com.lenchy.lms.util.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.homolo.datamodel.manager.EntityManager;
import com.homolo.framework.setup.Initializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import javax.validation.constraints.NotNull;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 數據緩存抽象類
*
* @param <T> 緩存值的類型
*/
public abstract class BaseDataCache<T> extends Initializer {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseDataCache.class);
private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
@Autowired
private TaskExecutor taskExecutor;
@Autowired
protected EntityManager entityManager;
private LoadingCache<String, T> cache;
/**
* @return 時間
*/
protected abstract long duration();
/**
* @return 時間單位
*/
protected abstract TimeUnit unit();
/**
* 根據key生成緩存值的方法
*
* @param key key
* @return 緩存值
*/
protected abstract T getCacheValue(String key);
/**
* 緩存到期是否自動刷新
*/
protected boolean refresh() {
return false;
}
/**
* 預加載緩存數據,系統啓動後執行
*
* @return Runnable 返回 null 表示不執行預加載
*/
protected Runnable preload() {
return null;
}
/**
* 獲取緩存值,請在初始化完成之後調用
*
* @param key key
* @return 緩存值,異常時返回 getCacheValue()
*/
public T get(String key) {
try {
return cache.get(key);
} catch (ExecutionException e) {
LOGGER.error("get cache error", e);
return getCacheValue(key);
}
}
@Override
public void initialize() {
long duration = duration();
TimeUnit unit = unit();
cache = CacheBuilder.newBuilder().refreshAfterWrite(duration(), unit())
.build(new CacheLoader<String, T>() {
@Override
public T load(@NotNull String key) {
return getCacheValue(key);
}
@Override
public ListenableFuture<T> reload(final String key, T oldValue) throws Exception {
ListenableFutureTask<T> task = ListenableFutureTask.create(new Callable<T>() {
public T call() {
return getCacheValue(key);
}
});
taskExecutor.execute(task);
return task;
}
});
Runnable preload = preload();
if (preload != null) {
LOGGER.info("preload {}", this.getClass().getSimpleName());
taskExecutor.execute(preload());
}
if (refresh()) {
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
for (String key : cache.asMap().keySet()) {
cache.refresh(key);
}
}
}, duration, duration, unit);
}
}
@Override
public int getPhase() {
return 20000;
}
}
簡單實現類
@Component
public class Statistic4JusticeCache extends BaseDataCache<Statistic4JusticeCache.JusticeCache> {
@Autowired
private JusticeBureauManager justiceBureauManager;
@Autowired
protected NationalDataCountUtil nationalDataStatisticsUtil;
@Autowired
private JusticeBureauFilter justiceBureauFilter;
@Override
protected long duration() {
return 1;
}
@Override
protected TimeUnit unit() {
return TimeUnit.DAYS;
}
@Override
protected boolean refresh() {
return true;
}
@Override
protected Runnable preload() {
return new Runnable() {
@Override
public void run() {
List<String> cacheKwys = new ArrayList<>();
cacheKwys.add( "first" );
cacheKwys.add( "second" );
cacheKwys.add( "third" );
for (String key : cacheKwys) {
get(key);
}
}
};
}
@Override
protected Statistic4JusticeCache.JusticeCache getCacheValue(String key) {
JusticeCache cache = new JusticeCache();
cache.setFirstList(managerGetList(key));
cache.setSecondList(getZoneList(key));
cache.setThirdMap(getLawyerModifymap(key));
return cache;
}
public static class JusticeCache {
private List firstList;
private List secondList;
private Map thirdMap;
public List getFirstList() {
return firstList;
}
public void setFirstList(List firstList) {
this.firstList = firstList;
}
public List getSecondlist() {
return secondList;
}
public void setSecondList(List secondList) {
this.secondList = secondList;
}
public Map getThirdMap() {
return thirdMap;
}
public void setThirdMap(Map thirdMap) {
this.thirdMap = thirdMap;
}
}
public List<String> getFirstList(){
List first = new ArrayList<>();
first.add("first1");
first.add("first2");
return first;
}
public List<String> getSecondlist(){
List second = new ArrayList<>();
first.add("second1");
first.add("second2");
return first;
}
public Map<String,String> getThirdMap(){
Map<String, String> third = new HashMap<String, String>();
third.put( "third1" , "third1" );
third.put( "third2" , "third2" );
return third;
}
}
有不足的地方希望大家指出。。。。。