前言
今天第一次使用MarkDown的形式發博客. 准備記錄一下自己對Guava Cache的認識及項目中的實際使用經驗.
一: 什么是Guava
Guava工程包含了若干被Google的 Java項目廣泛依賴 的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [primitives support] 、並發庫 [concurrency libraries] 、通用注解 [common annotations] 、字符串處理 [string processing] 、I/O 等等。 所有這些工具每天都在被Google的工程師應用在產品服務中。
//Guava Cache的使用
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
...
return graphs.getUnchecked(key);
二: 使用場景
當我們使用一種新工具的時候 我們總要先弄清楚它到底適用於什么樣的場景.
- 你願意消耗一些內存空間來提升速度。
- 你預料到某些鍵會被查詢一次以上。
- 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。如果這不符合你的需求,請嘗試Memcached這類工具)
如果你的場景符合上述的每一條,Guava Cache就適合你。
三: 核心類圖
四: 使用實例
前面說了這么多, 都不如如何使用來的實在. 現在直接貼出來使用的實例, 具體實現的邏輯大家可以看下源碼, 這里也會有一些實際的講解.
在pom文件中引入Guava Cache的坐標:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
下面拿我們實際項目中使用的一個GuavaCache來舉例:
public abstract class BaseCacheService<K,V> {
private LoadingCache<K,V> cache;
public BaseCacheService(){
cache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(new CacheLoader<K, V>() {
@Override
public V load(K k) throws Exception {
return loadData(k);
}
});
}
public BaseCacheService(long duration){
cache = CacheBuilder.newBuilder()
.expireAfterWrite(duration, TimeUnit.MINUTES)
.build(new CacheLoader<K, V>() {
@Override
public V load(K k) throws Exception {
return loadData(k);
}
});
}
protected abstract V loadData(K k);
public V getCache(K param){
return cache.getUnchecked(param);
}
//更新緩存中數據
public void refresh(K k){
cache.refresh(k);
}
}
這里我是抽象出來了一個BaseCacheService, 當我們使用時則可以繼承這個抽象類:
如果我們第一次請求, 那么這會執行這里面的load方法去數據庫中查詢相應的值, 當第二次請求時這會從緩存中直接返回了.
@Service
public class MaterialInfoCacheService extends BaseCacheService<Long, List<MaterialInfoDto>> {
@Override
protected List<MaterialInfoDto> loadData(Long key) {
//具體的查詢數據庫得到數據的邏輯.
return materialInfoDtos;
}
}
這里面有關於緩存的回收(expireAfterWrite), 有關於緩存的刷新(refresh)等, 這些東西會一一來介紹.
緩存的回收:
1, 基於容量的回收(size-based eviction)
如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。
另外,不同的緩存項有不同的“權重”(weights)——例如,如果你的緩存值,占據完全不同的內存空間,你可以使用CacheBuilder.weigher(Weigher)指定一個權重函數,並且用CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存創建時計算的,因此要考慮重量計算的復雜度。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
2, 定時回收(Timed Eviction)
CacheBuilder提供兩種定時回收的方法:
- expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。
- expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
3, 基於引用的回收(Reference-based Eviction)
通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:
- CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(),使用弱引用鍵的緩存用而不是equals比較鍵。
- CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(),使用弱引用值的緩存用而不是equals比較值。
- CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。
其實這里使用最多的還是基於時間的定時回收, 其他的兩種回收方式大家可以根據自己的項目而定.
緩存的顯示刷新和清除:
(任何時候,你都可以顯式地清除緩存項,而不是等到它被回收)
這里需要說明下刷新(refresh)和清除(invalidate)的區別:
刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個過程可以是異步的。在刷新操作進行時,
緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。
如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄 .
- 刷新: Cache.refresh(K k)
- 個別清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有緩存項:Cache.invalidateAll()
三: 使用實例
這里更新下我在項目中常用的guava cache的實例. 更新於2016年12月14日.
LoadingCache<String, Map<Long, CarAttentionDTO>> cache = CacheBuilder.newBuilder()
.expireAfterAccess(30, TimeUnit.MINUTES)
.build(new CacheLoader<String, Map<Long, CarAttentionDTO>>() {
public Map<Long, CarAttentionDTO> load(String key) { // no checked exception
LOGGER.info("loading car week attention data......");
long startTime = System.currentTimeMillis();
List<String> groupBy = Lists.newArrayList();
groupBy.add("key2");
Map<String, String> where = Maps.newHashMap();
where.put("group_name", String.valueOf(CommonConstants.CounterGroup.ATTENTION));
where.put("key1", String.valueOf(CommonConstants.DataType.CAR));
Calendar cal = Calendar.getInstance();
Date dateTo = DateUtils.addDays(cal.getTime(), -1);
Date dateFrom = DateUtils.addDays(cal.getTime(), -8);
int dayTo = Integer.valueOf(DateFormatUtils.format(dateTo, "yyyyMMdd"));
int dayFrom = Integer.valueOf(DateFormatUtils.format(dateFrom, "yyyyMMdd"));
List<CountDayUvEntity> list = uvEntityDao.countByParams(groupBy, where, dayFrom, dayTo);
int multiple = configReader.getInt(CommonConstants.SystemConfigKey.ATTENTION_MULTIPLE, 53);
Map<Long, CarAttentionDTO> tempMap = Maps.newHashMap();
for (CountDayUvEntity uvEntity : list) {
CarAttentionDTO attentionDTO = new CarAttentionDTO();
attentionDTO.setCarId(Long.valueOf(uvEntity.getKey2()));
attentionDTO.setAttention(uvEntity.getCount() * multiple + RandomUtils.nextInt(0, 10));
tempMap.put(attentionDTO.getCarId(), attentionDTO);
}
LOGGER.info("load car week attention finished. useTime=" + (System.currentTimeMillis() - startTime));
return tempMap;
}
});
private Cache<String, Object> carIndexCache = CacheBuilder.newBuilder().expireAfterAccess(20, TimeUnit.MINUTES).build();
public Map<Long, Long> getCarAttentions() throws ExecutionException {
String key = "getCarAttentions";
return (Map<Long, Long>) carIndexCache.get(key, new Callable<Map<Long, Long>>() {
@Override
public Map<Long, Long> call() throws Exception {
List<CarIndexEntity> carIndexs = carIndexEntityDao.findAll(
CarIndexEntity.Fields.type.eq(CommonConstants.CarIndexStatus.ATTENTION));
Map<Long, Long> data = Maps.newHashMapWithExpectedSize(carIndexs.size());
for (CarIndexEntity carIndex : carIndexs) {
data.put(carIndex.getCarId(), carIndex.getCount());
}
return data;
}
});
}
public Map<Long, Long> getCarSales() throws ExecutionException {
String key = "getCarSales";
return (Map<Long, Long>) carIndexCache.get(key, new Callable<Map<Long, Long>>() {
@Override
public Map<Long, Long> call() throws Exception {
List<CarIndexEntity> carIndexs = carIndexEntityDao.findAll(
CarIndexEntity.Fields.type.eq(CommonConstants.CarIndexStatus.SALES));
Map<Long, Long> data = Maps.newHashMapWithExpectedSize(carIndexs.size());
for (CarIndexEntity carIndex : carIndexs) {
data.put(carIndex.getCarId(), carIndex.getCount());
}
return data;
}
});
}
其實兩種情況都是一樣的, 第二個是使用場景是一個service有多個方法都需要用到guava cache.
好了 知道了這些就可以在項目中直接使用了, 更多的內容請看Guava Cache官方文檔(翻譯版):http://ifeve.com/google-guava-cachesexplained/