Spring Cache擴(kuò)展功能實(shí)現(xiàn)過(guò)程解析
兩個(gè)需求緩存失效時(shí)間支持在方法的注解上指定
Spring Cache默認(rèn)是不支持在@Cacheable上添加過(guò)期時(shí)間的,可以在配置緩存容器時(shí)統(tǒng)一指定:
@Bean public CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { CustomizedRedisCacheManager cacheManager= new CustomizedRedisCacheManager(redisTemplate); cacheManager.setDefaultExpiration(60); Map<String,Long> expiresMap=new HashMap<>(); expiresMap.put("Product",5L); cacheManager.setExpires(expiresMap); return cacheManager; }
想這樣配置過(guò)期時(shí)間,焦點(diǎn)在value的格式上Product#5#2,詳情下面會(huì)詳細(xì)說(shuō)明。
@Cacheable(value = {"Product#5#2"},key ="#id")
上面兩種各有利弊,并不是說(shuō)哪一種一定要比另外一種強(qiáng),根據(jù)自己項(xiàng)目的實(shí)際情況選擇。
在緩存即將過(guò)期時(shí)主動(dòng)刷新緩存
一般緩存失效后,會(huì)有一些請(qǐng)求會(huì)打到后端的數(shù)據(jù)庫(kù)上,這段時(shí)間的訪問(wèn)性能肯定是比有緩存的情況要差很多。所以期望在緩存即將過(guò)期的某一時(shí)間點(diǎn)后臺(tái)主動(dòng)去更新緩存以確保前端請(qǐng)求的緩存命中率,示意圖如下:
Srping 4.3提供了一個(gè)sync參數(shù)。是當(dāng)緩存失效后,為了避免多個(gè)請(qǐng)求打到數(shù)據(jù)庫(kù),系統(tǒng)做了一個(gè)并發(fā)控制優(yōu)化,同時(shí)只有一個(gè)線程會(huì)去數(shù)據(jù)庫(kù)取數(shù)據(jù)其它線程會(huì)被阻塞。
背景
我以Spring Cache +Redis為前提來(lái)實(shí)現(xiàn)上面兩個(gè)需求,其它類型的緩存原理應(yīng)該是相同的。
本文內(nèi)容未在生產(chǎn)環(huán)境驗(yàn)證過(guò),也許有不妥的地方,請(qǐng)多多指出。
擴(kuò)展RedisCacheManagerCustomizedRedisCacheManager
繼承自RedisCacheManager,定義兩個(gè)輔助性的屬性:
/** * 緩存參數(shù)的分隔符 * 數(shù)組元素0=緩存的名稱 * 數(shù)組元素1=緩存過(guò)期時(shí)間TTL * 數(shù)組元素2=緩存在多少秒開始主動(dòng)失效來(lái)強(qiáng)制刷新 */ private String separator = "#"; /** * 緩存主動(dòng)在失效前強(qiáng)制刷新緩存的時(shí)間 * 單位:秒 */ private long preloadSecondTime=0;
注解配置失效時(shí)間簡(jiǎn)單的方法就是在容器名稱上動(dòng)動(dòng)手腳,通過(guò)解析特定格式的名稱來(lái)變向?qū)崿F(xiàn)失效時(shí)間的獲取。比如第一個(gè)#后面的5可以定義為失效時(shí)間,第二個(gè)#后面的2是刷新緩存的時(shí)間,只需要重寫getCache:
- 解析配置的value值,分別計(jì)算出真正的緩存名稱,失效時(shí)間以及緩存刷新的時(shí)間
- 調(diào)用構(gòu)造函數(shù)返回緩存對(duì)象
@Override public Cache getCache(String name) { String[] cacheParams=name.split(this.getSeparator()); String cacheName = cacheParams[0]; if(StringUtils.isBlank(cacheName)){ return null; } Long expirationSecondTime = this.computeExpiration(cacheName); if(cacheParams.length>1) { expirationSecondTime=Long.parseLong(cacheParams[1]); this.setDefaultExpiration(expirationSecondTime); } if(cacheParams.length>2) { this.setPreloadSecondTime(Long.parseLong(cacheParams[2])); } Cache cache = super.getCache(cacheName); if(null==cache){ return cache; } logger.info("expirationSecondTime:"+expirationSecondTime); CustomizedRedisCache redisCache= new CustomizedRedisCache( cacheName, (this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null), this.getRedisOperations(), expirationSecondTime, preloadSecondTime); return redisCache; }
CustomizedRedisCache
主要是實(shí)現(xiàn)緩存即將過(guò)期時(shí)能夠主動(dòng)觸發(fā)緩存更新,核心是下面這個(gè)get方法。在獲取到緩存后再次取緩存剩余的時(shí)間,如果時(shí)間小余我們配置的刷新時(shí)間就手動(dòng)刷新緩存。為了不影響get的性能,啟用后臺(tái)線程去完成緩存的刷新。
public ValueWrapper get(Object key) { ValueWrapper valueWrapper= super.get(key); if(null!=valueWrapper){ Long ttl= this.redisOperations.getExpire(key); if(null!=ttl&& ttl<=this.preloadSecondTime){ logger.info("key:{} ttl:{} preloadSecondTime:{}",key,ttl,preloadSecondTime); ThreadTaskHelper.run(new Runnable() { @Override public void run() { //重新加載數(shù)據(jù) logger.info("refresh key:{}",key); CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(),key.toString()); } }); } } return valueWrapper; }
ThreadTaskHelper是個(gè)幫助類,但需要考慮重復(fù)請(qǐng)求問(wèn)題,及相同的數(shù)據(jù)在并發(fā)過(guò)程中只允許刷新一次,這塊還沒(méi)有完善就不貼代碼了。
攔截@Cacheable,并記錄執(zhí)行方法信息
上面提到的緩存獲取時(shí),會(huì)根據(jù)配置的刷新時(shí)間來(lái)判斷是否需要刷新數(shù)據(jù),當(dāng)符合條件時(shí)會(huì)觸發(fā)數(shù)據(jù)刷新。但它需要知道執(zhí)行什么方法以及更新哪些數(shù)據(jù),所以就有了下面這些類。
CacheSupport
刷新緩存接口,可刷新整個(gè)容器的緩存也可以只刷新指定鍵的緩存。
public interface CacheSupport { /** * 刷新容器中所有值 * @param cacheName */ void refreshCache(String cacheName); /** * 按容器以及指定鍵更新緩存 * @param cacheName * @param cacheKey */ void refreshCacheByKey(String cacheName,String cacheKey); }
InvocationRegistry
執(zhí)行方法注冊(cè)接口,能夠在適當(dāng)?shù)牡胤街鲃?dòng)調(diào)用方法執(zhí)行來(lái)完成緩存的更新。
public interface InvocationRegistry { void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set<String> cacheNames); }
CachedInvocation
執(zhí)行方法信息類,這個(gè)比較簡(jiǎn)單,就是滿足方法執(zhí)行的所有信息即可。
public final class CachedInvocation { private Object key; private final Object targetBean; private final Method targetMethod; private Object[] arguments; public CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) { this.key = key; this.targetBean = targetBean; this.targetMethod = targetMethod; if (arguments != null && arguments.length != 0) { this.arguments = Arrays.copyOf(arguments, arguments.length); } } }
CacheSupportImpl
這個(gè)類主要實(shí)現(xiàn)上面定義的緩存刷新接口以及執(zhí)行方法注冊(cè)接口
刷新緩存
獲取cacheManager用來(lái)操作緩存:
@Autowired private CacheManager cacheManager;
實(shí)現(xiàn)緩存刷新接口方法:
@Override public void refreshCache(String cacheName) { this.refreshCacheByKey(cacheName,null); } @Override public void refreshCacheByKey(String cacheName, String cacheKey) { if (cacheToInvocationsMap.get(cacheName) != null) { for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) { if(!StringUtils.isBlank(cacheKey)&&invocation.getKey().toString().equals(cacheKey)) { refreshCache(invocation, cacheName); } } } }
反射來(lái)調(diào)用方法:
private Object invoke(CachedInvocation invocation) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { final MethodInvoker invoker = new MethodInvoker(); invoker.setTargetObject(invocation.getTargetBean()); invoker.setArguments(invocation.getArguments()); invoker.setTargetMethod(invocation.getTargetMethod().getName()); invoker.prepare(); return invoker.invoke(); }
緩存刷新最后實(shí)際執(zhí)行是這個(gè)方法,通過(guò)invoke函數(shù)獲取到最新的數(shù)據(jù),然后通過(guò)cacheManager來(lái)完成緩存的更新操作。
private void refreshCache(CachedInvocation invocation, String cacheName) { boolean invocationSuccess; Object computed = null; try { computed = invoke(invocation); invocationSuccess = true; } catch (Exception ex) { invocationSuccess = false; } if (invocationSuccess) { if (cacheToInvocationsMap.get(cacheName) != null) { cacheManager.getCache(cacheName).put(invocation.getKey(), computed); } } }
執(zhí)行方法信息注冊(cè)
定義一個(gè)Map用來(lái)存儲(chǔ)執(zhí)行方法的信息:
private Map<String, Set<CachedInvocation>> cacheToInvocationsMap;
實(shí)現(xiàn)執(zhí)行方法信息接口,構(gòu)造執(zhí)行方法對(duì)象然后存儲(chǔ)到Map中。
@Override public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments, Set<String> annotatedCacheNames) { StringBuilder sb = new StringBuilder(); for (Object obj : arguments) { sb.append(obj.toString()); } Object key = sb.toString(); final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments); for (final String cacheName : annotatedCacheNames) { String[] cacheParams=cacheName.split("#"); String realCacheName = cacheParams[0]; if(!cacheToInvocationsMap.containsKey(realCacheName)) { this.initialize(); } cacheToInvocationsMap.get(realCacheName).add(invocation); } }
CachingAnnotationsAspect
攔截@Cacheable方法信息并完成注冊(cè),將使用了緩存的方法的執(zhí)行信息存儲(chǔ)到Map中,key是緩存容器的名稱,value是不同參數(shù)的方法執(zhí)行實(shí)例,核心方法就是registerInvocation。
@Around("pointcut()") public Object registerInvocation(ProceedingJoinPoint joinPoint) throws Throwable{ Method method = this.getSpecificmethod(joinPoint); List<Cacheable> annotations=this.getMethodAnnotations(method,Cacheable.class); Set<String> cacheSet = new HashSet<String>(); for (Cacheable cacheables : annotations) { cacheSet.addAll(Arrays.asList(cacheables.value())); } cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method, joinPoint.getArgs(), cacheSet); return joinPoint.proceed(); }
客戶端調(diào)用
指定5秒后過(guò)期,并且在緩存存活3秒后如果請(qǐng)求命中,會(huì)在后臺(tái)啟動(dòng)線程重新從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)來(lái)完成緩存的更新。理論上前端不會(huì)存在緩存不命中的情況,當(dāng)然如果正好最后兩秒沒(méi)有請(qǐng)求那也會(huì)出現(xiàn)緩存失效的情況。
@Cacheable(value = {"Product#5#2"},key ="#id") public Product getById(Long id) { //... }
代碼
可以從項(xiàng)目中下載。
引用
刷新緩存的思路取自于這個(gè)開源項(xiàng)目。https://github.com/yantrashala/spring-cache-self-refresh
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Springboot整合GuavaCache緩存過(guò)程解析
- SpringBoot加入Guava Cache實(shí)現(xiàn)本地緩存代碼實(shí)例
- 詳解Guava Cache本地緩存在Spring Boot應(yīng)用中的實(shí)踐
- springboot使用GuavaCache做簡(jiǎn)單緩存處理的方法
- Springboot使用cache緩存過(guò)程代碼實(shí)例
- Java內(nèi)存緩存工具Guava LoadingCache使用解析
- SpringBoot集成cache緩存的實(shí)現(xiàn)
- 解析springboot整合谷歌開源緩存框架Guava Cache原理
相關(guān)文章
Spring mvc如何實(shí)現(xiàn)數(shù)據(jù)處理
這篇文章主要介紹了Spring mvc如何實(shí)現(xiàn)數(shù)據(jù)處理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java基于FFmpeg實(shí)現(xiàn)Mp4視頻轉(zhuǎn)GIF
FFmpeg是一套可以用來(lái)記錄、轉(zhuǎn)換數(shù)字音頻、視頻,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序。本文主要介紹了在Java中如何基于FFmpeg進(jìn)行Mp4視頻到Gif動(dòng)圖的轉(zhuǎn)換,感興趣的小伙伴可以了解一下2022-11-11Java,C#使用二進(jìn)制序列化、反序列化操作數(shù)據(jù)
這篇文章主要介紹了Java,C#使用二進(jìn)制序列化、反序列化操作數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2014-10-10Springsession nginx反向代理集成過(guò)程
這篇文章主要介紹了Springsession nginx反向代理集成過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04SpringBoot2.0 整合 SpringSecurity 框架實(shí)現(xiàn)用戶權(quán)限安全管理方法
Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問(wèn)控制解決方案的安全框架。這篇文章主要介紹了SpringBoot2.0 整合 SpringSecurity 框架,實(shí)現(xiàn)用戶權(quán)限安全管理 ,需要的朋友可以參考下2019-07-07