基於多執行緒和本地快取實現跨域重複呼叫的高效能
>> 最近工作了,寫部落格的時間越來越少了,思考的時間越來越少,學習沉澱的時間也越來越少。忙裡偷閒,記錄一些在平時工作中一些有亮點的小tip,記錄一些實用的技能,也多虧平時接觸到的有能力有想法的同事~
前言
對於大體量網際網路公司的應用,更多場景下需要考慮效能問題,比如大促穩定性、高併發下HA等等。而且在目前微服務盛行的今天,糟糕的涉及往往會導致鏈路的高rt和糟糕的效能,進而導致糟糕的使用者體驗。在平時的碼程式碼的過程中,要多注意這方面,不能一味的懟程式碼,而應該通過一些或高階或實用的方式提高效能。
現在有一種case:對於目前我們的系統應用A,高程度依賴了一個外部系統B的服務,但是糟糕的設計導致現在的程式碼中存在多個迴圈的同步服務呼叫以及一次請求中多次重複的跨域呼叫兩個問題,導致應用A的部分介面rt較高,效能不夠好。
針對這個問題,有幾個優化思路,也是很常見的優化思路:多執行緒、加快取。但是如何設計,如何通過工具化的方式開放能力給更多應用使用呢?
解決
一、結合CompletionService,通過多執行緒的方式優化同步迴圈查詢
CompletionService將Executor和BlockingQueue的功能整合在了一起,我們就可以使用類似於佇列操作的take和poll方法來獲取一個已完成的future,多執行緒查詢優化查詢效能。
思路:通過配置的方式,將執行緒池相關bean託管給spring來管理,利用執行緒池批量查詢。當這個工具包作為一個jar被引入時,需要結合spring的一些註解對配置和bean進行管理。
解決步驟1:
- ApplicationContextAware獲取springcontext
public class SpringContextHelper implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextHelper.applicationContext = applicationContext; } public static ApplicationContext getContext(){ return applicationContext; } }
-
@ConfigurationProperties(prefix="xxx")獲得和執行緒池相關的配置資訊;這些資訊可由jar包使用方來進行配置,spring會自動搜尋application.properties下匹配的配置項並注入到bean中
@ConfigurationProperties(
prefix = "xxx.xxx.xxxx"
)
...
@Configuration
@ConditionalOnProperty(
prefix = "xxx.xxx.xxxx",
name = {"enabled"},
havingValue = "true"
)
@EnableConfigurationProperties({XxxxxxxxProperties.class})
@ConfigurationProperties註解主要用來把properties配置檔案轉化為bean來使用,注意需要保證和application.properties裡面配置的配置key一致;
@Configuration註解類似於原先xml檔案中的<beans>註解,通過這個類中的@Bean註解將bean託管於spring;
@EnableConfigurationProperties,將被@ConfigurationProperties註解的類注入為spring容器中的bean;
通過spring.factories完成spring-boot啟動時的bean載入,將被@Configuration註解的類例項化為bean託管給spring
至此,已經完成基本的準備工作,下面開始建立多執行緒相關bean:
@Bean("xxxxxExecutor")
public ThreadPoolTaskExecutor getExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(properties.getMaxPoolSize());
executor.setQueueCapacity(properties.getQueueCapacity());
executor.setKeepAliveSeconds(properties.getKeepAliveSeconds());
executor.setCorePoolSize(properties.getCorePoolSize());
executor.setThreadNamePrefix("xxx");
return executor;
}
其中重要的屬性通過properties注入到當前bean,建立ThreadPoolTaskExecutor,進一步建立CompletionService的實現類ExecutorCompletionService。
批量get方法:
public static <T> List<T> batchQuery(List<Callable<T>> jobList) {
List<T> result = new ArrayList<>();
CompletionService<T> completionService = new ExecutorCompletionService<>(
getExecutor());
for (Callable<T> job : jobList) {
completionService.submit(job);
}
for (int i = 0; i < jobList.size(); i++) {
try {
Future<T> task = completionService.poll(getTimeout(), 3000);
if (task != null) {
result.add(task.get(getTimeout(), 3000));
}
} catch (Exception e) {
throw ExceptionUtils.newInstance(e);
}
}
return result;
}
CompletionService這個類還是蠻有意思的,下次看看原始碼。
解決步驟2:解決同請求中多次重複跨域呼叫的問題,奔著簡單實用的思路,我們用ThreadLocal來做。
- 定義切面aspect,@EnableCache,通過註解的方式對需要做快取的方法做切面處理。該註解放在存在多次重複跨域呼叫的方法入口處,在切面的實現裡主要完成了初始化ThreadLocal並在finally裡clear掉;切面bean同樣通過@Configuration的配置類bean來注入spring
- cacheQuery方法:
public static <T> T cacheQuery(Supplier<String> getKey, Supplier<T> noCacheQuery) {
String key = getKey.get();
T result;
Map<String, Object> dataMap = getDataMap();
if (dataMap != null) {
result = (T)dataMap.get(key);
if (result == null) {
result = noCacheQuery.get();
dataMap.put(key, result);
}
}else {
result = noCacheQuery.get();
}
return result;
}
- 在封裝服務的時候,注意ThreadLocal中key的唯一性