有朋友問我一個關於接口優化的問題,他的優化點很清晰,因爲接口中調用了內部不少的 service 去組成了一個完成的業務功能。每一個 service 中的邏輯都是獨立的,這樣就致使了不少查詢是重複的,看下圖你就明白了。git
上層查詢傳遞下去
對於這種場景最好的就是在上層將須要的數據查詢出來,而後傳遞到下層去消費。這樣就不用重複查詢了。github
若是開始寫代碼的時候是這樣作的沒問題,但不少時候,以前寫的時候都是獨立的,或者複用的老邏輯,裏面就是有獨立的查詢。數據庫
若是要作優化就只能將老的方法重載一個,將須要的信息直接傳遞過去。緩存
public void xxx(int goodsId) { Goods goods = goodsService.get(goodsId); .....}public void xxx(Goods goods) { .....}加緩存
若是你的業務場景容許數據有必定延遲,那麼重複調用你能夠直接經過加緩存來解決。這樣的好處在於不會重複查詢數據庫,而是直接從緩存中取數據。微信
更大的好處在於對於優化類的影響最小,原有的代碼邏輯都不用改變,只須要在查詢的方法上加註解進行緩存便可。ide
public void xxx(int goodsId) { Goods goods = goodsService.get(goodsId); .....}public void xxx(Goods goods) { Goods goods = goodsService.get(goodsId); .....}class GoodsService { @Cached(expire = 10, timeUnit = TimeUnit.SECONDS) public Goods get(int goodsId) { return dao.findById(goodsId); }}
若是你的業務場景不容許有緩存的話,上面這個方法就不能用了。那麼是否是還得改代碼,將須要的信息一層層往下傳遞呢?優化
自定義線程內的緩存咱們總結下目前的問題:lua
總結後發現這個場景適合用 ThreadLocal 來傳遞數據,對已有代碼改動量最小,並且也只對當前線程生效,不會影響其餘線程。線程
public void xxx(int goodsId) { Goods goods = ThreadLocal.get(); if (goods == null) { goods = goodsService.get(goodsId); } .....}
上面代碼就是使用了 ThreadLocal 來獲取數據,若是有的話就直接使用,不用去從新查詢,沒有的話就去查詢,不影響老邏輯。code
雖然能實現效果,可是不太好,不夠優雅。也不夠通用,若是一次請求內要緩存多種類型的數據怎麼處理? ThreadLocal 就不能存儲固定的類型。還有就是老的邏輯仍是得改,加了個判斷。
下面介紹一種比較優雅的方式:
注意:ThreadLocal 不能跨線程,若是有跨線程需求,請使用阿里的 ttl 來裝飾。
註解定義
@Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface ThreadLocalCache { /** * 緩存key,支持SPEL表達式 * @return */ String key() default ""; }存儲定義
/** * 線程內緩存管理 * * @做者 尹吉歡 * @時間 2020-07-12 10:47 */ public class ThreadLocalCacheManager { private static ThreadLocal<Map> threadLocalCache = new ThreadLocal<>(); public static void setCache(Map value) { threadLocalCache.set(value); } public static Map getCache() { return threadLocalCache.get(); } public static void removeCache() { threadLocalCache.remove(); } public static void removeCache(String key) { Map cache = threadLocalCache.get(); if (cache != null) { cache.remove(key); } }}切面定義
/** * 線程內緩存 * * @做者 尹吉歡 * @時間 2020-07-12 10:48 */ @Aspect public class ThreadLocalCacheAspect { @Around(value = "@annotation(localCache)") public Object aroundAdvice(ProceedingJoinPoint joinpoint, ThreadLocalCache localCache) throws Throwable { Object[] args = joinpoint.getArgs(); Method method = ((MethodSignature) joinpoint.getSignature()).getMethod(); String className = joinpoint.getTarget().getClass().getName(); String methodName = method.getName(); String key = parseKey(localCache.key(), method, args, getDefaultKey(className, methodName, args)); Map cache = ThreadLocalCacheManager.getCache(); if (cache == null) { cache = new HashMap(); } Map finalCache = cache; Map<String, Object> data = new HashMap<>(); data.put("methodName", className + "." + methodName); Object cacheResult = CatTransactionManager.newTransaction(() -> { if (finalCache.containsKey(key)) { return finalCache.get(key); } return null; }, "ThreadLocalCache", "CacheGet", data); if (cacheResult != null) { return cacheResult; } return CatTransactionManager.newTransaction(() -> { Object result = null; try { result = joinpoint.proceed(); } catch (Throwable throwable) { throw new RuntimeException(throwable); } finalCache.put(key, result); ThreadLocalCacheManager.setCache(finalCache); return result; }, "ThreadLocalCache", "CachePut", data); } private String getDefaultKey(String className, String methodName, Object[] args) { String defaultKey = className + "." + methodName; if (args != null) { defaultKey = defaultKey + "." + JsonUtils.toJson(args); } return defaultKey; } private String parseKey(String key, Method method, Object[] args, String defaultKey){ if (!StringUtils.hasText(key)) { return defaultKey; } LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = nameDiscoverer.getParameterNames(method); ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); for(int i = 0;i < paraNameArr.length; i++){ context.setVariable(paraNameArr[i], args[i]); } try { return parser.parseExpression(key).getValue(context, String.class); } catch (SpelEvaluationException e) { // 解析不出SPEL默認爲類名+方法名+參數 return defaultKey; } } }過濾器定義
/** * 線程緩存過濾器 * * @做者 尹吉歡 * @我的微信 jihuan900 * @微信公衆號 猿天地 * @GitHub https://github.com/yinjihuan * @做者介紹 http://cxytiandi.com/about * @時間 2020-07-12 19:46 */ @Slf4j public class ThreadLocalCacheFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { filterChain.doFilter(servletRequest, servletResponse); // 執行完後清除緩存 ThreadLocalCacheManager.removeCache(); } }自動配置類
@Configuration public class ThreadLocalCacheAutoConfiguration { @Bean public FilterRegistrationBean idempotentParamtFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); ThreadLocalCacheFilter filter = new ThreadLocalCacheFilter(); registration.setFilter(filter); registration.addUrlPatterns("/*"); registration.setName("thread-local-cache-filter"); registration.setOrder(1); return registration; } @Bean public ThreadLocalCacheAspect threadLocalCacheAspect() { return new ThreadLocalCacheAspect(); }}使用案例
@Service public class TestService { /** * ThreadLocalCache 會緩存,只對當前線程有效 * @return */ @ThreadLocalCache public String getName() { System.out.println("開始查詢了"); return "yinjihaun"; } /** * 支持SPEL表達式 * @param id * @return */ @ThreadLocalCache(key = "#id") public String getName(String id) { System.out.println("開始查詢了"); return "yinjihaun" + id; }}
功能代碼: github.com/yinjihuan/k…
案例代碼: github.com/yinjihuan/k…