>> 最近工作了,写博客的时间越来越少了,思考的时间越来越少,学习沉淀的时间也越来越少。忙里偷闲,记录一些在平时工作中一些有亮点的小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的唯一性