一种解决图表数据过多的接口方案
当需要进行前端数据展示的时候,图形和表格是非常有用的利器。但是,最近在工作中遇到了一个问题,那就是在某些情况下,服务端需要返回大量的数据。另外,由于工作限制,没有直接使用echarts和highcharts,但是该方案不仅仅是前端页面绘制的问题。
数据量大的问题
超过了网关的限制
在微服务体系下,前端一般会直接同网关接口交互,然后再由网关将请求转发到真正的服务端。所以,网关需要对传入的内容(比如body和header等)进行解析。为了解析效率,通常需要对body大小进行限制(如2M),超过之后,就会拦截。
针对这种情况,可以通过分页或者加时间条件,缩写数据查询的范围。在无法缩写返回结果大小的情况下,会有2种解决方案。
-
将该类接口,作为类似数据下载的网关接口类型。
很明显,数据下载类接口,不会受到body大小的限制,因为这类下载接口,数据大小很容易超过2M。
但是,因为数据量大,网关无法再对服务端返回内容进行解析,进而完成返回值的参数映射工作。
如果服务端的接口返回响应和网关通用的响应不一致(比如,服务端叫data,网关叫Data),这种情况下,就
需要前端单独处理,不便于后续的维护。
-
采用异步下载的方案。
收到查询请求后,将数据获取之后,放置到类似阿里云对象存储里面,再由前端去对象存储中获取。
该方案会对交互有很大改动,毕竟是从同步修改为异步,改造成本比较大。
此外,我们的场景中,由于查询范围的多样性,每次都需要将数据存储到对象存储中,数据几乎没有被复用,对象存储有些浪费。
前端组件的渲染问题
当数据量特别多的时候,此时对于前端来说,需要在同一个页面上,进行绘制。所以,业务使用的组件需要对数据进行过滤渲染,但是具体的渲染效果却不理想,比如1s的数据,颗粒度是ms,那对于服务端返回的1000个点,业务组件会按照某种规律丢弃点,造成整体的曲线不够圆滑。
解决方案
由服务端根据数据特点,结合前端实际的渲染能力和网关的限制,设置好要切割的数据范围个数,按照计算类型(比如求和、求平均值)对每一个范围的数据点进行聚合操作。这样可以保证,无论查询条件如何,都会返回统一大小的数据点集合。
示例代码
/**
* 对原始数据进行auto scale操作
* @param originData 原始数据
* @param startTime 开始时间
* @param endTime 截止时间
* @param splitSize 分隔的大小
* @param aggType 计算类型 sum或者Agg
* @return scale 后的结果
* 此时结果会统一将值转为String类型
*/
public static List<CommonTimeData<Double>> autoScale(List<CommonTimeData<Long>> originData,
Long startTime, Long endTime, int splitSize, CommonTimeDataAggType aggType) {
if (CollectionUtils.isEmpty(originData)) {
return Lists.newArrayList();
}
CommonTimeData<Long> firstData = originData.get(0);
CommonTimeData<Long> lastData = originData.get(originData.size() - 1);
long minStartTime = firstData.getTimestamp();
long maxEndTime = lastData.getTimestamp();
if (null != startTime) {
minStartTime = startTime;
}
if (null != endTime) {
maxEndTime = endTime;
}
// 如果时间戳范围小于要切割的大小,则不进行切割
if (maxEndTime - minStartTime < splitSize) {
return originData.stream().map(value -> {
CommonTimeData<Double> commonTimeData = new CommonTimeData<>();
commonTimeData.setTimestamp(value.getTimestamp());
commonTimeData.setValue(Double.parseDouble(String.valueOf(value)));
return commonTimeData;
}).collect(Collectors.toList());
}
long delta = (maxEndTime - minStartTime) / splitSize;
List<CommonTimeData<Double>> autoScale = Lists.newArrayListWithCapacity(originData.size());
int startIndex = 0;
// 总共会有splitSize个数据点
for (int i = 0; i < splitSize; i++) {
double aggValue = 0L;
long currentDataTimeStamp = minStartTime + delta * i;
long count = 0;
// 对该范围内的点进行统计汇总
for (int j = startIndex; j < originData.size(); j++) {
CommonTimeData<Long> data = originData.get(j);
if (data.getTimestamp() - currentDataTimeStamp <= delta) {
long value = data.getValue();
aggValue = aggValue + value;
count = count + 1;
} else {
startIndex = j;
break;
}
}
CommonTimeData<Double> newData = new CommonTimeData<>();
newData.setTimestamp(currentDataTimeStamp);
// 由于是浮点数,对返回数据进行2位小数的格式化处理
DecimalFormat decimalFormat = new DecimalFormat("#.##");
switch (aggType) {
case SUM:
// 求和
String formatSumValue= decimalFormat.format(aggValue);
newData.setValue(Double.parseDouble(formatSumValue));
break;
case AVG:
// 求平均值
double avgValue = 0;
if (count > 0) {
avgValue = aggValue / count;
}
String formatAvgValue= decimalFormat.format(avgValue);
newData.setValue(Double.parseDouble(formatAvgValue));
break;
default:
break;
}
autoScale.add(newData);
}
return autoScale;
}
存在的问题:
- 如果原数据为long类型,但是计算类型存在平均值的情况,会有类型转换,不过对于前端绘制数据,没有实际影响。