日常開發中,某些資料介面即使優化到極致,都難免還會存在計算量巨大導致響應過慢,多數情況單獨做一個統計表用於存放這些處理後的資料用於讀取,或者接入redis/memcache存資料,就是說單次響應本身是可以接受較慢一些的,實時性並非特別高,則可以考慮引入快取機制,提升使用體驗。說到用快取,那就會有人提出用redis,但是專案組認為專案緊急,不希望浪費時間到新的工具研究上,或雖然熟悉,但維護工作有成本,為了有限的效果付出太多不划算。那麼怎麼辦,沒得搞了,只能手把手給專案做一個快取工具了!吃掉JVM!也和spring cache很類似的。
這樣的快取機制,無非就是key-value模型的體現,所以首先想到了map。
Map<String, Object> cache = new HashMap<>();
一個快取工具就完成了,快吧。怎麼用的話,就類似這樣嘛:
@GetMapping("/{id}") public Object get(@PathVariable("id") String id) { if (cache.containKey(id)) { return cache.get(id); } // 調取服務獲取物件 Object obj = service.get(id); // 塞進快取中 cache.put(id, obj); return obj; }
挺好用的,既方便效果又達成。
但是一想到是JVM的記憶體,那麼物件是存放在堆中的,一旦發生GC,資料被清掉了呢,會不會在下面這個操作的時候,我containKey判斷它的確存在,但是到了return的時候,應該返回的物件沒有了。
if (cache.containKey(id)) { return cache.get(id); }
這的確是個問題,需要防止它發生。有辦法,換個思路寫上面的程式碼:
Object obj = cache.get(id); if (obj != null) { return obj; }
這樣寫總可以了吧,物件真的存在的時候我才給直接返回,不然還是老老實實執行查詢物件的方法。
好是挺好,但是總不至於每次一個類,我就得它new一個Map吧,那得多費Map,而且Map可以存在很多的物件在裡面,只要它的Key不重複。
考慮下Spring的元件處理,其實也是抽Map成類,當然可以將Map放到一個類中做靜態屬性欄位。Map的重複利用算是解決了。但是快取的資料不是一直都不變的,那還需要給它來一個定時,刷走快取資料。
@Component public Cache<String, Object> extends HashMap<String, Object> { @Scheduled(cron="0 0/5 * * * ?") public void flushCache() { clear(); } }
其實這樣做還是不夠靈活,應該更能定製化地刷走快取,有些資料是5分鐘才變化,但有些資料一天都不變呢。這樣的話,可以考慮用Java的定時器,對上面進行優化,定時刪除指定的資料。
快取的地方有了,定時重新整理有了,但是仍然不好用,因為每個方法我都需要寫程式碼去判斷是否存在快取。為了解決這個問題,很自然地想到了加註解,AOP做切面,交給切面去處理,很快,就做出來了。
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CachePut { String key() default ""; }
再把切面實現下:
@Aspect @Component public class CachePutAspect { @Autowired private Cache cache; @Pointcut("@annotation(com.lin.cache.Cache)") public void cachePointCut() { } @SuppressWarnings("rawtypes") @Around("cachePointCut()") public Object pointCut(ProceedingJoinPoint pjp) throws Exception { Object result = null; String methodName = pjp.getSignature().getName(); Class<?> classTarget = pjp.getTarget().getClass(); Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes(); Method method = classTarget.getMethod(methodName, par); CachePut cachePut = method.getAnnotation(CachePut.class); if (cachePut != null) { result = cache.get(cacheImport.key()); // 避免獲取結果時遇上clear操作 if (result != null) { return result; } try { result = pjp.proceed(); // 將結果快取 cacheMap.put(key, result); } catch (Throwable throwable) { throw new Exception("方法錯誤"); } } return result; } }
使用的時候就是這樣的了。
@CachePut(key = "getId") @GetMapping("/{id}") public Object get(@PathVariable("id") String id) { // 調取服務獲取物件 Object obj = service.get(id); return obj; }
好像還是不對勁的,因為key都是固定是"getId",豈不是每個不同物件都被覆蓋掉了,但肯定不行,那怎麼辦。想起來之前用redis做快取的時候,用的spring的快取@CachePut(key = "info + #id"),從方法的入參那裡拿到唯一的標識,將快取結果區分開來,這裡網上的資料比較少,啃原始碼一下子沒看明白,怎麼想都覺得這個操作很簡單。直到偶爾翻翻,找到有人提供了一個可行的例子,才得以讓這個快取工具得到昇華。
@Aspect @Component public class CachePutAspect { @Autowired private Cache cache; @Pointcut("@annotation(com.lin.cache.Cache)") public void cachePointCut() { } @SuppressWarnings("rawtypes") @Around("cachePointCut()") public Object pointCut(ProceedingJoinPoint pjp) throws Exception { Object result = null; String methodName = pjp.getSignature().getName(); Class<?> classTarget = pjp.getTarget().getClass(); Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes(); Method method = classTarget.getMethod(methodName, par); CachePut cachePut = method.getAnnotation(CachePut.class); if (cachePut != null) { String key = generateKeyBySpEL(cacheImport.key(), pjp); result = cacheMap.get(key); // 避免獲取結果時遇上clear操作 if (result != null) { return result; } try { result = pjp.proceed(); // 將結果快取 cacheMap.put(key, result); } catch (Throwable throwable) { throw new Exception("方法錯誤"); } } return result; } // 使用SpringEL,將入引數據和表示式繫結起來,得到Key public String generateKeyBySpEL(String key, ProceedingJoinPoint pjp) { Expression expression = parserSpel.parseExpression(key); EvaluationContext context = new StandardEvaluationContext(); MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Object[] args = pjp.getArgs(); String[] paramNames = parameterNameDiscoverer .getParameterNames(methodSignature.getMethod()); for(int i = 0 ; i < args.length ; i++) { context.setVariable(paramNames[i], args[i]); } return expression.getValue(context).toString(); } }
現在就可以這樣用靈活的註解了:
@CachePut(key = "'info-' + #id)
差不多了完成這個快取工具了,當然除了將結果放進快取的操作用註解處理,把快取移除的操作也可以用註解完成。這裡就不實現了。
enn。。好像和Spring Cache的比較像,將就吧。