Guava本地緩存托底緩存以及異步更新緩存
1.簡介
- 1.1 guava本地緩存是開發中比較常用的組件,一般使用 LoadingCache,將需要的值加載在內存中,如下所示
LoadingCache<String,T> cacheLoader= CacheBuilder
.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, T>() {
@Override
public T load(String key) throws Exception {
return method(key);//1.執行method,獲取key對應的值
}
});
使用的方法:
T value=cacheLoader.get(key);//獲取key對應的值
2.托底緩存設置
- 如果mehod()執行出錯的話,無法拿到新的緩存。有時候,我們希望如果method執行異常的時候,本地緩存依舊用過期的緩存,那么可以重寫CacheLoader中的reload方法進行設置
public abstract class RefreshKeepCacheLoader<K, V> extends CacheLoader<K, V> {
public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
Object newvalue = null;
try {
newvalue = this.load(key);
} catch (Exception e) {
}
if(newvalue == null) {
newvalue = oldValue;
}
return Futures.immediateFuture(newvalue);
}
}
那么此時我們的cacheLoader可以這么寫:
LoadingCache<String,T> cacheLoader= CacheBuilder
.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new RefreshKeepCacheLoader<String, T>() {
@Override
public T load(String key) throws Exception {
return method(key);//2.執行method,獲取key對應的值
}
});
與上面不同的是,用自定義的RefreshKeepCacheLoader替換了CacheLoader類,由於緩存過期會執行reload方法,如果reload異常,就采用oldValue。
3.異步緩存設置
- 3.1在並發條件下,有N個線程,如果緩存失效了,會有一個線程A去執行load方法(參見官方說明文檔If another call to get(K) or getUnchecked(K) is currently loading the value for key, simply waits for that thread to finish and returns its loaded value.),而其他線程就會等待線程A得到的結果,這樣就會影響性能。
- 3.2 我們可以使全部線程返回舊的緩存值,把去異步更新緩存。方法如下:
首先我們看下,默認的reload的方法是怎么寫的:
public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
checkNotNull(key);
checkNotNull(oldValue);
return Futures.immediateFuture(load(key));
}
其實通過方法名,就可以看出執行load方法,然后用Futures.immediateFuture封裝成ListenableFuture,再來看下immediateFuture方法
/**
* Creates a {@code ListenableFuture} which has its value set immediately upon construction. The
* getters just return the value. This {@code Future} can't be canceled or timed out and its
* {@code isDone()} method always returns {@code true}.
*/
public static <V> ListenableFuture<V> immediateFuture(@Nullable V value) {
if (value == null) {
// This cast is safe because null is assignable to V for all V (i.e. it is covariant)
@SuppressWarnings({"unchecked", "rawtypes"})
ListenableFuture<V> typedNull = (ListenableFuture) ImmediateSuccessfulFuture.NULL;
return typedNull;
}
return new ImmediateSuccessfulFuture<V>(value);
}
這就比較清楚了,這個future中直接設置的是值。
那么我們現在就要重寫這個reload方法。
public abstract class RefreshAsyncCacheLoader<K, V> extends CacheLoader<K, V> {
@Override
public ListenableFuture<V> reload(final K key, final V oldValue) throws Exception {
ListenableFutureTask<V> task = ListenableFutureTask.create(new Callable<V>() {
public V call() {
try {
return load((K) key);
} catch (Exception e) {
}
return oldValue;
}
});
ThreadPoolUtil.getInstance().execute(task);//這里將這個task放到自定義的線程池中去執行,返回一個futrue,可以通過future獲取線程執行獲取的值
return task;
}
}
4.總結
- 最終我們可以得到具有托底緩存設置,並且可以異步更新緩存的guavaCache
LoadingCache<String,T> cacheLoader= CacheBuilder
.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new RefreshAsyncCacheLoader <String, T>() {
@Override
public T load(String key) throws Exception {
return method(key);//1.執行method,獲取key對應的值
}
});
5.實驗
下面我們做一個簡單的實驗,涉及到WebController,CacheTest這兩個類。
package com.netease.mail.activity.web.controller;
import com.netease.mail.activity.constant.RetCode;
import com.netease.mail.activity.meta.RequestHolder;
import com.netease.mail.activity.meta.vo.common.AjaxResult;
import com.netease.mail.activity.service.CacheTest;
import com.netease.mail.rambo.log.StatLogger;
import com.netease.mail.rambo.log.StatLoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* 重構web-control
* Created by hzlaojiaqi on 2016/9/19.
*/
@Controller
public class WebController {
private static final Logger INTERACTIVE_LOG = LoggerFactory.getLogger("INTERACTIVE_LOGGER");
public static final StatLogger USER_TRACE_LOG = StatLoggerFactory.getLogger("emptyProject");
@Autowired
RequestHolder mRequestHolder;
@Autowired
CacheTest cacheTest;
/**
* 獲取當前uid的信息
* @param httpServletRequest
* @return
*/
@RequestMapping(value = "/ajax/getActInfo.do",method = RequestMethod.GET)
@ResponseBody
public AjaxResult getActInfo2(HttpServletRequest httpServletRequest){
String uid = mRequestHolder.getUid();
return new AjaxResult(RetCode.SUCCESS,cacheTest.cacheGet(uid));
}
}
- 在看下CacheTest這個service
package com.netease.mail.activity.service;
import com.netease.mail.activity.cache.RedisConfigure;
import com.netease.mail.activity.web.controller.BaseAjaxController;
import com.netease.mail.util.common.TimeUtil;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* Created by hzlaojiaqi on 2017/11/9.
*/
@Service
public class CacheTest extends BaseService{
@Cacheable(cacheManager = "activityCacheManager",key = "'actCache_'+#uid",cacheNames = "actCache",unless = "#result==null")
public String cacheGet(String uid){
String s = uid + "_" + TimeUtil.now();
THIRDPARTY_LOG.info("cacheGet in method uid:{} s:{}",uid,s);
return s;
}
}
- 這里設置緩存的時間比較短,只有10s鍾,具體的設置如下:
LoadingCache<String,Object> cache = CacheBuilder
.newBuilder()
.maximumSize(5210)
.refreshAfterWrite(10, TimeUnit.SECONDS)
.build(new RefreshAsyncCacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
Object o = mRedis.opsForValue().get(key);
THIRDPARTY_LOG.debug("thread:{},activityManager local cache reload key:{},result:{}",Thread.currentThread().getName(),key,o);
return o;
}
});
- 最終可以看到的log為
- 請求線程為http-apr-8080-exec-8,該線程馬上返回了原來的值,而我們自定義的線程custom_thread_member_11則執行了reload方法。(這里加載后的值和之前的值是一樣的,特此說明)
- 如果是第一次請求,由於沒有舊的值,那么http-apr-8080-exec-8會去執行reload方法。
Ps:文章難免有紕漏,望拍磚指正,感謝。