您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關SpringCache如何實現請求級別緩存,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
要將數據緩存在一次請求周期內,那我們先得區分是什么環境下的請求,以分析我們如何存儲數據。
Web環境下的有個絕佳的數據存儲位置 HttpServletRequest的Attribute 。調用setAttribute和getAttribute方法就能輕易地將我們的數據用key-value的形式存儲在請求上,而且每次請求都自動擁有一個干凈的Request。想要獲取到HttpServletRequest 也非常簡單,在web請求中隨時隨地調用((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest() 即可。
我司所使用的rpc框架是基于finagle自研的,對外提供服務時使用線程池進行處理請求,即對于一次完整的請求,會使用同一個線程進行處理。首先想到的辦法還是改動這個rpc框架服務端,增加一個可以對外暴露的、可以key-value存儲的請求上下文。為了能在方便的地方獲取到這個請求上下文,得將其存儲在ThreadLocal中。
綜合這兩種環境考慮,我們最好還是實現一個統一的方案以減少維護和開發成本。Spring的RequestContextHolder.getRequestAttributes()其實也是使用ThreadLocal來實現的,那我們可以統一將數據存到ThreadLocal<Map<Object,Object>>,自己來維護緩存的清理。
存儲位置有了,接下來實現SpringCache思路就比較清晰了。
要實現SpringCache需要一個CacheManager,接口定義如下
xxxxxxxxxx
public interface CacheManager {
Cache getCache(String name);
Collection<String> getCacheNames();
}
可以看到其實只需要實現Cache接口就行了。 在上一篇文章中提到的SimpleCacheManager,它的Cache實現ConcurrentMapCache內部的存儲是依賴ConcurrentMap<Object, Object>。我們的實現跟它非常類似,最主要的不同是我們需要使用ThreadLocal<Map<Object, Object>> 下面給出幾處關鍵的實現,其他部分簡單看下ConcurrentMapCache就能明白。
我們選擇不直接繼承Cache而是AbstractValueAdaptingCache,其被大多數緩存實現所繼承,它的作用主要是包裝value值以區分是沒有命中緩存還是緩存的null值。
xxxxxxxxxx
private final ThreadLocal<Map<Object, Object>> store = ThreadLocal.withInitial(() -> new HashMap<>(128));
我們的緩存數據存儲的地方,ThreadLocal保證緩存只會存在于這一個線程中。同時又因為只有一個線程能夠訪問,我們簡單地使用HashMap即可。
xxxxxxxxxx
public <T> T get(Object key, Callable<T> valueLoader) {
return (T) fromStoreValue(this.store.get().computeIfAbsent(key, r -> {
try {
return toStoreValue(valueLoader.call());
} catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
}));
}
至此我們即將大功告成,只差一個步驟,ThreadLocal的清理:使用AOP實現即可。
xxxxxxxxxx
@After("bean(server)")
public void clearThreadCache() {
threadCacheManager.clear();
}
記得將Cache的clear方法通過我們自定義的CacheManager暴露出來。同時也要確保切面能覆蓋每個請求的結束。
從以上一個簡單的ThreadLocalCacheManager實現,我們對CacheManager又有了更多的理解。
同時可能也會有更多的疑問。
再回顧Spring Cache為我們提供的@Cacheable中的sync的注釋,它提到此功能的作用是: 同步化對被注解方法的調用,使得多個線程試圖調用此方法時,只有一個線程能夠成功調用,其他線程直接取這次調用的返回值。同時也提到這僅僅只是個hint,是否真的能成還是要看緩存提供者。
我們找到Spring Cache處理緩存調用的關鍵方法org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.reflect.Method, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts) (spring-context-5.1.5.RELEASE)
經過分析,當sync = true 時, 只會調用如下代碼
xxxxxxxxxx
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))))
即我們上文實現的T get(Object key, Callable<T> valueLoader) 方法,回頭一看一切都清晰了。 只要我們的this.store.get().computeIfAbsent是同步的,那這個sync = true就起作用了。 當然我們這里使用的HashMap不支持,但是我們如果換成ConcurrentMap就能夠實現同步化的功能。另外簡單粗暴地讓方法同步也是可以的(RedisCache就是這樣做的)。
當sync = false時,會組合Cache中其他的方法進行緩存的處理。邏輯較為簡單清晰,自行閱讀源碼即可。
異步操作分兩種情況,直接創建線程或者使用線程池。對于第一種情況我們可以簡單地使用java.lang.InheritableThreadLocal 來替代ThreadLocal,創建的子進程會自然而然地共享父進程的InheritableThreadLocal;第二種情況就相對比較復雜了,建議可以參考 alibaba/transmittable-thread-local ,它實現了線程池下的ThreadLocal值傳遞功能。
關于“SpringCache如何實現請求級別緩存”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。