1. 程式人生 > >簡直騷操作,ThreadLocal還能當快取用

簡直騷操作,ThreadLocal還能當快取用

## 背景說明 有朋友問我一個關於介面優化的問題,他的優化點很清晰,由於介面中呼叫了內部很多的 service 去組成了一個完成的業務功能。每個 service 中的邏輯都是獨立的,這樣就導致了很多查詢是重複的,看下圖你就明白了。 ![](https://img2020.cnblogs.com/blog/1618095/202008/1618095-20200810125708144-1756122254.png) ## 上層查詢傳遞下去 對於這種場景最好的就是在上層將需要的資料查詢出來,然後傳遞到下層去消費。這樣就不用重複查詢了。 ![](https://img2020.cnblogs.com/blog/1618095/202008/1618095-20200810125721848-1764760935.png) 如果開始寫程式碼的時候是這樣做的沒問題,但很多時候,之前寫的時候都是獨立的,或者複用的老邏輯,裡面就是有獨立的查詢。 如果要做優化就只能將老的方法過載一個,將需要的資訊直接傳遞過去。 ```plain public void xxx(int goodsId) { Goods goods = goodsService.get(goodsId); ..... } public void xxx(Goods goods) { ..... } ``` ## 加快取 如果你的業務場景允許資料有一定延遲,那麼重複呼叫你可以直接通過加快取來解決。這樣的好處在於不會重複查詢資料庫,而是直接從快取中取資料。 更大的好處在於對於優化類的影響最小,原有的程式碼邏輯都不用改變,只需要在查詢的方法上加註解進行快取即可。 ```plain 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); } } ``` 如果你的業務場景不允許有快取的話,上面這個方法就不能用了。那麼是不是還得改程式碼,將需要的資訊一層層往下傳遞呢? ## 自定義執行緒內的快取 我們總結下目前的問題: 1. 同一次請求內,多次相同的查詢獲取 RPC 等的呼叫。 2. 資料實時性要求高,不適合加快取,主要是加快取也不好設定過期時間,除非採用資料變更主動更新快取的方式。 3. 只需要在這一次請求裡快取即可,不影響其他地方。 4. 不想改動已有程式碼。 總結後發現這個場景適合用 ThreadLocal 來傳遞資料,對已有程式碼改動量最小,而且也只對當前執行緒生效,不會影響其他執行緒。 ```plain public void xxx(int goodsId) { Goods goods = ThreadLocal.get(); if (goods == null) { goods = goodsService.get(goodsId); } ..... } ``` 上面程式碼就是使用了 ThreadLocal 來獲取資料,如果有的話就直接使用,不用去重新查詢,沒有的話就去查詢,不影響老邏輯。 雖然能實現效果,但是不太好,不夠優雅。也不夠通用,如果一次請求內要快取多種型別的資料怎麼處理? ThreadLocal 就不能儲存固定的型別。還有就是老的邏輯還是得改,加了個判斷。 下面介紹一種比較優雅的方式: 1. 自定義快取註解,加在查詢的方法上。 2. 定義切面切到加了快取註解的方法上,第一次獲取返回值存入 ThreadLocal。第二次直接從 ThreadLocal 中取值返回。 3. ThreadLocal 中儲存 Map,Key 為某方法的某一標識,這樣可以快取多種型別的結果。 4. 在 Filter 中將 ThreadLocal 進行 remove 操作,因為執行緒是複用的,使用完需要清空。 注意:ThreadLocal 不能跨執行緒,如果有跨執行緒需求,請使用阿里的 ttl 來裝飾。 ![](https://img2020.cnblogs.com/blog/1618095/202008/1618095-20200810125736229-2058413761.png) ### 註解定義 ```plain @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface ThreadLocalCache { /** * 快取key,支援SPEL表示式 * @return */ String key() default ""; } ``` ### 儲存定義 ```plain /** * 執行緒內快取管理 * * @作者 尹吉歡 * @時間 2020-07-12 10:47 */ public class ThreadLocalCacheManager { private static ThreadLocal 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); } } } ``` ### 切面定義 ```plain /** * 執行緒內快取 * * @作者 尹吉歡 * @時間 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