需求
在150万数据的表中(日增50万),按照不同维度进行字段统计数据。接口返回。
接口响应时间差不多6秒以上。
惨不忍睹
分析优化步骤
分析
分析从两个方面来分析接口
- 查询个数
- 数据实时性要求
- SQL索引使用
查询接口性能优化
一个接口里尽量将数据查询减少到最低,最好就1个或者2个查询,不超过4个。这个查询是指DB查询或者第三方依赖接口查询。减少这样的查询,无非就是为了减少接口因为过多的查询,造成接口响应时间过大。那么有以下两种场景,根据实际场景选择合适的解决方案。
场景1:跨库查询多,即依赖于其他项目的接口多,可以选择使用并发查询,也就是使用线程池来减少接口总共所消耗的时间。原因在于,不涉及自己项目数据库连接池资源的竞争,并发查询可以有效地提高接口的响应能力。
场景2:本库查询多,可以选择使用缓存查询。比如将变动小的数据存放在redis缓存中;而变动较为频繁的数据,根据评估风险,可以选择将删除缓存和查询存放缓存进行配套使用,即在数据更新的时候对应删除缓存,保证查询的时候是最新的数据;原因在于,本库的查询较多,涉及到数据库连接池资源的竞争,如果使用并发查询也必须有空闲的数据库连接可用,否则并发查询也无太大效果。因此存放进缓存是最有效也是最合适的方式。
————————————————
版权声明:本文为CSDN博主「隐者自怡悦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/YYQ_QYY/article/details/105996347
SQL索引使用
明确使用我们想要使用的SQL索引。数据库层面当作SQL最优处理。
查询个数
代码逻辑中,分为按维度查询,和总计查询。使用同步的方式调用SQL查询。分别用时2秒,3秒。
数据实时性要求
数据是查询前一天的数据。在不同类型查询全量数据的时候,数据的实时性要求很低。而且反馈接口慢的情况也只是查询全量数据数据。
优化
- 查询个数,同步耗时时间长。
这里采用异步的方式调用两个查询。接口响应时间也差不多缩短了1/2.如果单纯只改变为异步调用的情况。响应时间为3秒左右,还是没办法接受。 - 数据缓冲
由于全量数据查询(前一天数据已经落库,一般不进行修改),数据实时性要求并不高。所以在进行全量数据查询的时候进行缓冲处理。
目前的状态做到以上
第一次查询的时候肯定还是很慢(3s,异步处理之后的结果),之后全量查询查询全是走缓冲。
异步处理之后的结果【第一次查询】
缓冲处理之后结果
Mark
并发查询数据库连接池资源的竞争问题。第一次查询慢的问题。
都可以通过建立定时调度的。在每天数据库更新最新数据之后,调度全量查询接口,将数据进行缓冲。解决以上两个问题。
代码
异步调用
配置异步线程池 ExecutorConfig
package com.fang.industry.service.config;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@EnableAsync // 开启对异步任务的支持
@Configuration
public class ExecutorConfig implements AsyncConfigurer {
private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Autowired
private BeanFactory beanFactory;
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
// 设置核心线程数
threadPool.setCorePoolSize(20);
// 设置最大线程数
threadPool.setMaxPoolSize(1000);
// 线程池所使用的缓冲队列
threadPool.setQueueCapacity(10);
// 等待任务在关机时完成--表明等待所有线程执行完
threadPool.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间 (默认为0,此时立即停止),并没等待xx秒后强制停止
threadPool.setAwaitTerminationSeconds(60);
// 线程名称前缀
threadPool.setThreadNamePrefix("MyAsync-");
// 初始化线程
threadPool.initialize();
return new LazyTraceExecutor(beanFactory, threadPool);
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncExceptionHandler();
}
/**
* 自定义异常处理类
*
* @author hry
*
*/
class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
// 手动处理捕获的异常
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
if (!"E.api.response.50001".equals(throwable.getMessage())) {
System.out.println("-------------》》》捕获线程异常信息");
logger.info("Exception message - " + throwable.getMessage());
logger.info("Method name - " + method.getName());
for (Object param : obj) {
logger.info("Parameter value - " + param);
}
}
}
}
}
注入异步线程池
@Autowired
private Executor executor;
使用方式
/**
* 异步调用在3秒左右
*/
CompletableFuture<List<LandSupplyStatisticsDto>> listCompletableFuture = CompletableFuture.supplyAsync(() -> {
// 指标获取的语句
return landDataDao.getLandSupplyStatisticsAll(landSupplyStatistics);
}, executor);
CompletableFuture<List<LandSupplyStatisticsDto>> listCompletableFuture1 = CompletableFuture.supplyAsync(() -> {
// 指标获取的语句
return landDataDao.getLandSupplyStatistics(landSupplyStatistics);
}, executor);
try {
//阻塞等待返回结果方式
List<LandSupplyStatisticsDto> landSupplyStatisticsAll = listCompletableFuture.get();
List<LandSupplyStatisticsDto> dtos = listCompletableFuture1.get();
landSupplyStatisticsAll.addAll(dtos);
return landSupplyStatisticsAll;
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
return null;
存在子线程太慢情况
使用join等待所有子线程运行结束
// join等待所有线程执行完成
CompletableFuture.allOf(uCompletableFuture, mapCompletableFuture, mapCompletableFuture1,mapCompletableFuture2,mapCompletableFuture3).join();
缓冲数据
注入缓冲
@Autowired
private RedisTemplate<String, Object> redisTemplate;
写入缓冲
/**
* 全量数据按QueryType存缓存【支持1,3,4】
* 第二天早晨9点过期
* @author guochao.bj@fang.com
* @date 2021/8/12
*/
private void addCache(LandSupplyStatisticsBo bo,List<LandSupplyStatisticsDto> landSupplyStatisticsAll){
String key="industry-effect-api-LandSupplyStatistics-"+bo.getQueryType();
redisTemplate.opsForValue().set(key,landSupplyStatisticsAll,getSecondsNextEarlyMorning(), TimeUnit.SECONDS);
}
从缓冲中取
/**
* 根据类型从缓冲中获取全量数据
* @author guochao.bj@fang.com
* @date 2021/8/12
*/
private List<LandSupplyStatisticsDto> getDataFromCache(LandSupplyStatisticsBo bo){
String key="industry-effect-api-LandSupplyStatistics-"+bo.getQueryType();
if (redisTemplate.opsForValue().get(key) == null) {
return null;
}else {
List<LandSupplyStatisticsDto> landSupplyStatisticsDtos = (List<LandSupplyStatisticsDto>) redisTemplate.opsForValue().get(key);
return landSupplyStatisticsDtos;
}
}
整体调用逻辑
public List<LandSupplyStatisticsDto> getLandSupplyStatistics(LandSupplyStatisticsBo bo) {
if (isFulfillCache(bo)){ //是否是全量数据缓冲
List<LandSupplyStatisticsDto> dataFromCache = getDataFromCache(bo); //从缓冲取
if (dataFromCache!=null){ //缓冲中有,返回
return dataFromCache;
}else { //缓冲中没有,使用数据库查询,并存入缓冲
List<LandSupplyStatisticsDto> landSupplyStatisticsFromDB = getLandSupplyStatisticsFromDB(bo);
addCache(bo,landSupplyStatisticsFromDB);
return landSupplyStatisticsFromDB;
}
}else {
List<LandSupplyStatisticsDto> landSupplyStatisticsFromDB = getLandSupplyStatisticsFromDB(bo);
return landSupplyStatisticsFromDB;
}
}
工具代码
- 获取当前时间到第二天9点的秒数
private static long getSecondsNextEarlyMorning(){
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 1);
cal.set(Calendar.HOUR_OF_DAY, 9);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.MILLISECOND, 0);
/**
*验证使用,https://www.baidu.com/s?ie=UTF-8&wd=%E7%A7%92%E8%BD%AC%E6%97%B6
*/
Date time = cal.getTime();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(time));
return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
}
- 判断字符串是否数字
/**
* 判断字符串是否数字
* @param str
* @return boolean
* @author guochao.bj@fang.com
* @date 2021/8/12
*/
public boolean isNumeric(String str) {
Pattern pattern = Pattern.compile("^[-+]?(([0-9]+)([.]([0-9]+))?|([.]([0-9]+))?)$");
Matcher isNum = pattern.matcher(str);
return isNum.matches();
}
- 获取前一天日期
/**
* 获取前一天日期
* @return java.lang.String
* @author guochao.bj@fang.com
* @date 2021/8/12
*/
private String getYesterdayStr(){
Date dNow = new Date();
Date dBefore = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(dNow);
calendar.add(Calendar.DAY_OF_MONTH, -1);
dBefore = calendar.getTime();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(dBefore);
}