Spring Cache擴充套件功能實現過程解析
兩個需求快取失效時間支援在方法的註解上指定
Spring Cache預設是不支援在@Cacheable上新增過期時間的,可以在配置快取容器時統一指定:
@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; }
想這樣配置過期時間,焦點在value的格式上Product#5#2,詳情下面會詳細說明。
@Cacheable(value = {"Product#5#2"},key ="#id")
上面兩種各有利弊,並不是說哪一種一定要比另外一種強,根據自己專案的實際情況選擇。
在快取即將過期時主動重新整理快取
一般快取失效後,會有一些請求會打到後端的資料庫上,這段時間的訪問效能肯定是比有快取的情況要差很多。所以期望在快取即將過期的某一時間點後臺主動去更新快取以確保前端請求的快取命中率,示意圖如下:
Srping 4.3提供了一個sync引數。是當快取失效後,為了避免多個請求打到資料庫,系統做了一個併發控制優化,同時只有一個執行緒會去資料庫取資料其它執行緒會被阻塞。
背景
我以Spring Cache +Redis為前提來實現上面兩個需求,其它型別的快取原理應該是相同的。
本文內容未在生產環境驗證過,也許有不妥的地方,請多多指出。
擴充套件RedisCacheManagerCustomizedRedisCacheManager
繼承自RedisCacheManager,定義兩個輔助性的屬性:
/** * 快取引數的分隔符 * 陣列元素0=快取的名稱 * 陣列元素1=快取過期時間TTL * 陣列元素2=快取在多少秒開始主動失效來強制重新整理 */ private String separator = "#"; /** * 快取主動在失效前強制重新整理快取的時間 * 單位:秒 */ private long preloadSecondTime=0;
註解配置失效時間簡單的方法就是在容器名稱上動動手腳,通過解析特定格式的名稱來變向實現失效時間的獲取。比如第一個#後面的5可以定義為失效時間,第二個#後面的2是重新整理快取的時間,只需要重寫getCache:
- 解析配置的value值,分別計算出真正的快取名稱,失效時間以及快取重新整理的時間
- 呼叫建構函式返回快取物件
@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
主要是實現快取即將過期時能夠主動觸發快取更新,核心是下面這個get方法。在獲取到快取後再次取快取剩餘的時間,如果時間小余我們配置的重新整理時間就手動重新整理快取。為了不影響get的效能,啟用後臺執行緒去完成快取的重新整理。
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() { //重新載入資料 logger.info("refresh key:{}",key); CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(),key.toString()); } }); } } return valueWrapper; }
ThreadTaskHelper是個幫助類,但需要考慮重複請求問題,及相同的資料在併發過程中只允許重新整理一次,這塊還沒有完善就不貼程式碼了。
攔截@Cacheable,並記錄執行方法資訊
上面提到的快取獲取時,會根據配置的重新整理時間來判斷是否需要重新整理資料,當符合條件時會觸發資料重新整理。但它需要知道執行什麼方法以及更新哪些資料,所以就有了下面這些類。
CacheSupport
重新整理快取介面,可重新整理整個容器的快取也可以只重新整理指定鍵的快取。
public interface CacheSupport { /** * 重新整理容器中所有值 * @param cacheName */ void refreshCache(String cacheName); /** * 按容器以及指定鍵更新快取 * @param cacheName * @param cacheKey */ void refreshCacheByKey(String cacheName,String cacheKey); }
InvocationRegistry
執行方法註冊介面,能夠在適當的地方主動呼叫方法執行來完成快取的更新。
public interface InvocationRegistry { void registerInvocation(Object invokedBean,Method invokedMethod,Object[] invocationArguments,Set<String> cacheNames); }
CachedInvocation
執行方法資訊類,這個比較簡單,就是滿足方法執行的所有資訊即可。
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
這個類主要實現上面定義的快取重新整理介面以及執行方法註冊介面
重新整理快取
獲取cacheManager用來操作快取:
@Autowired private CacheManager cacheManager;
實現快取重新整理介面方法:
@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); } } } }
反射來呼叫方法:
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(); }
快取重新整理最後實際執行是這個方法,通過invoke函式獲取到最新的資料,然後通過cacheManager來完成快取的更新操作。
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); } } }
執行方法資訊註冊
定義一個Map用來儲存執行方法的資訊:
private Map<String,Set<CachedInvocation>> cacheToInvocationsMap;
實現執行方法資訊介面,構造執行方法物件然後儲存到Map中。
@Override public void registerInvocation(Object targetBean,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方法資訊並完成註冊,將使用了快取的方法的執行資訊儲存到Map中,key是快取容器的名稱,value是不同引數的方法執行例項,核心方法就是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(); }
客戶端呼叫
指定5秒後過期,並且在快取存活3秒後如果請求命中,會在後臺啟動執行緒重新從資料庫中獲取資料來完成快取的更新。理論上前端不會存在快取不命中的情況,當然如果正好最後兩秒沒有請求那也會出現快取失效的情況。
@Cacheable(value = {"Product#5#2"},key ="#id") public Product getById(Long id) { //... }
程式碼
可以從專案中下載。
引用
重新整理快取的思路取自於這個開源專案。https://github.com/yantrashala/spring-cache-self-refresh
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。