记录一次接口优化的过程。接口响应时间从500s下降到5s。
接口说明:
该接口通过用户导入的一年内每天的厂区用电功率数据来计算用户安装储能设备后的收益情况。
用电功率数据具体为每15分钟一条,一年约有 12*30*24*4 = 34560 条。
代码循环情况为:一层循环(根据经验,储能设备1-35台) 二层循环(月份,12) 三层循环(天,大约30)四层循环(根据业务模型,一天分为18个时间段)共计需要循环:35*12*30*18 = 226,800 次。
业务模型,一天分为18个时间段:
步骤:
一、分析代码,列出怀疑的耗时代码,对该段代码进行计时
1、怀疑点A 这段代码在主代码流程里(不参与循环),getDayPowerInfo方法就是一次性读取整年用电功率数据的方法,返回按月分组的的二维数组,二维数组里共有 (约)34560 条数据。
TimeInterval interval = DateUtil.timer();
System.err.println("开始计时:"+interval.start());
Map<String, Double[][]> monthAndDayPowerInfo = getDayPowerInfo(req.getCompanyId());
System.err.println("getDayPowerInfo耗时:"+interval.intervalSecond());
平均耗时:13秒
2、怀疑点B 这段代码在二层循环里(按月循环)
// 单天单台的理论收益值
Double Price_standard = provinceMonthIncomeService.calVoltageDayIncome(year + "", month + "",req.getVoltageId());
System.err.println("t_1 耗时:"+interval.intervalSecond());
平均耗时:240ms (乘以循环次数 35台12月之后,耗时100.8秒)
3、怀疑点C 这段代码在二层循环里(按月循环)
// 用电波形
Map<Long, List<BasedataElecRuleDO>> ruleMap = elecRuleService.getVoltageYearMonthElecRule(Arrays.asList(voltageId), year + "", month + "");
System.err.println("t_2:"+interval.intervalSecond());
平均耗时:70ms(乘以循环次数 35台12月之后,耗时29.4秒)
4、怀疑点D 这段代码在二层循环里(按月循环)
// 电价
Map<Long, List<BasedataElecPriceDO>> priceMap = elecPriceService.priceByVoltageIdList(year + "",month + "", Arrays.asList(voltageId));
System.err.println("t_3:"+interval.intervalSecond());
平均耗时:40ms(乘以循环次数 35台12月之后,耗时16.8秒)
5、怀疑点E 这段代码在二层循环里(按月循环)
// 实际用电量(根据波形)
Map<Integer, Double> elecTypeEnergyMap = getElecTypeEnergy(year + "", month + "", req.getVoltageId(),req.getCompanyId());
System.err.println("t_4:"+interval.intervalSecond());
平均耗时:460ms(乘以循环次数 35台12月之后,耗时193.2秒)
二、分析并进行优化
1、怀疑点A,这段代码主要耗时在查询3万多的数据,该表目前50w数据
原始SQL如下
SELECT
*
FROM
cal_company_load_data
WHERE
company_id = 50
AND deleted = 0
ORDER BY
load_date ASC
优化方法:
1)添加索引
2)减少查询的字段
SELECT
power,load_date
FROM
cal_company_load_data
WHERE
company_id = 50
AND deleted = 0
ORDER BY
load_date ASC
优化结果: 11s -> 3s
目前仍旧不是很满意,如有高手,请帮忙指正。
2、怀疑点B,这个代码主要耗时点为,2次查询SQL和1次查询外部接口
// 1. 获取年月电价
Map<Long, List<BasedataElecPriceDO>> voltagePriceMap =
priceService.priceByVoltageIdList(year, month, Arrays.asList(voltageId));
List<BasedataElecPriceDO> priceList = voltagePriceMap.get(voltageId);
// 2. 获取年月规则
Map<Long, List<BasedataElecRuleDO>> voltageRuleMap =
ruleService.getVoltageYearMonthElecRule(Arrays.asList(voltageId), year, month);
List<BasedataElecRuleDO> ruleList = voltageRuleMap.get(voltageId);
// 调用外部接口
HttpResponse response = HttpUtil.createPost(rankUrl.get(0).getName())
.header("Content-Type", "application/json; charset=UTF-8").body(jsonParam).execute();
优化方法:
1)电价和规则(波形)的获取,可以提取到代码主流程里进行统一查询,然后整合成一个map,以月份为key,再把map传入该方法使用,这样可以避免在月份的循环里去查SQL
//在主代码流程里进行统一查询
TreeMap<String, List<BasedataElecRuleDO>> voltageByMonth =elecRuleService.getVoltageYearElecRule(voltageId, yearS + "");
TreeMap<String, List<BasedataElecPriceDO>> priceByMonth =elecPriceService.getVoltageYearElecPrice(yearS + "", voltageId);
// 把整合的map传入该方法
Double Price_standard = provinceMonthIncomeService.calVoltageDayIncome(year + "", month + "",req.getVoltageId(),ruleMap,priceMap);
// 在方法中直接从map里取,不用再查数据库
// 1. 获取年月电价
List<BasedataElecPriceDO> priceList = voltagePriceMap.get(voltageId);
// 2. 获取年月规则
List<BasedataElecRuleDO> ruleList = voltageRuleMap.get(voltageId);
2) 查询外部接口暂时无法优化,耗时约50ms
优化结果:460ms -> 80ms (乘以循环次数 35台12月之后,耗时33.6秒)
3、怀疑C和怀疑点D,问题一样都是在二层循环里进行SQL查询
在对怀疑点B的优化中,其实我已经把对于C和D的查询放到里代码主流程里,然后整合成map在循环里使用,所以这里其实不用再优化了。
红色为原代码,方法里去查SQL了,蓝色为优化后代码,从map里取数据。
优化结果:40ms+70ms -> 1ms+1ms (乘以循环次数 35台12月之后,耗时<1秒)
4、怀疑点E 该方法主要是通读取用户导入的负载数据(3万多条那个)来计算用户实际使用的电能。
优化方法:
1) 这段代码经过上下游业务分析,发现与该接口业务不是强相关,完全可以单独形成一个接口,前端可以同时调用这2个接口,以减少页面等待的总时间。
// 分离出一个接口
@PostMapping(value = "/calEnergyUsed")
@ApiOperation("根据负载功率曲线计算实际用电量")
public CommonResult<Map<Integer, Double>> calEnergyUsed(Long companyId,Long voltageId) {
Map<Integer, Double>resp=companyLoadDataService.calEnergyUsed(companyId,voltageId);
return success(resp);
}
2) 该接口与怀疑点A一样,查询了3w条数据,所以也和A的优化方法一样,对SQL进行优化
优化结果:
460ms (注意这里是分月查询DB,乘以循环次数 35台12月之后,总耗时193.2秒)-> 5秒(注意这里是一次查询全年数据)
目前优化总结:
目前5个怀疑点的总耗时由 13秒+100.8秒+29.4秒+16.8秒+193.2秒 = 360 秒 ,优化到
3s +33.6秒+<1秒+5秒(并行,忽略) = 37 秒 似乎还是无法接受
三、进一步优化
经过分析,发现怀疑点B,还有优化空间。
怀疑点B 这段代码在二层循环里(按月循环)
// 单天单台的理论收益值
Double Price_standard = provinceMonthIncomeService.calVoltageDayIncome(year + "", month + "",req.getVoltageId());
System.err.println("t_1 耗时:"+interval.intervalSecond());
我们仔细观察B的代码,发现该B与一层循环没有数据上的关系,他只与二层循环(月份循环)有关。由于他比较耗时,也就是说,我们可以将此方法抽离出大的循环之外,以减少该方法的循环次数。简单计算一下:本来需要循环 35*12次,提取出来之后,只需循环12次。
// 在大循环之前,提前对每个月份的月理论收益值进行循环计算,整合成map,再传递到后面的循环里去使用
Map<Integer,Double> monthAndSaveTheory = new HashMap<>();
for (Map.Entry<String, Double[][]> stringEntry : monthAndDayPowerInfo.entrySet()) {
String yearAndMonth = stringEntry.getKey();
Integer year = Integer.parseInt(yearAndMonth.split("-")[0]);
Integer month = Integer.parseInt(yearAndMonth.split("-")[1]);
......
// 单天单台的理论收益值
Double Price_standard = provinceMonthIncomeService.calVoltageDayIncome(year+"", month + "",
req.getVoltageId(),ruleMap,priceMap);
monthAndSaveTheory.put(month, Price_standard);
}
优化结果:
33.6秒 -> 1秒
再次优化总结:
一次优化:3s +33.6秒+<1秒+5秒(并行,忽略) = 37 秒
二次优化:3s+1s+<1秒+5秒(并行,忽略) = 5秒
目前这个接口接口已经由500秒 优化到5秒
四、再进一步优化
目前看来,最大的耗时为3w条数据的SQL查询时间(3s),待续........