背景说明
最近在实现通过聊天机器人查询业绩时,当查询一年以上的业绩情况,需要处理百万级的数据量,导致响应时间极慢,一般在100秒以上 (暴查一天的业绩用时一般1s左右)
问题分析
public Response getIncomeDetailFn(long startTime, long endTime)
通过传入起始和结束时间戳,查询业绩
第一时间想到的优化方式方式是对查询结果做缓存(记忆化),下次查询如果缓存有结果就直接返回
但很明显不行! 因为查询的时间基本不会重复,比如第一次查23年1月到5月 第二次查22年5月到23年8月
对查询区间按年进行分段预处理
比如预处理出每年的业绩情况,下次查询22年3月到24年2月的业绩情况,则等于22年3月到12月、23年1月到12月、24年1月到2月的业绩情况相加,其中23年1月到12月的数据我们已经预处理了,就可以节省这一部分时间
但是!!!
假设查询23年1月到3月的业绩,仍然需要老老实实地读数据库进行计算,而一个月的业绩情况仍然需要查询5s以上
更细的分段?
如果把分段单位做得更细呢?比如以月为区间,分区处理每月的结果,那查询1月12号到5月12号的结果,则等于 1月12号 - 1月31号(计算)、2月、3月、4月(记忆化)、5月1号-5月12号(计算)
做到这一步,时间已经优化到5s左右了
还不够!!!
可以直接以天为单位预处理数据吗?
可以,但是仍不能解决问题: 我要查22年1月5日8:23:25 到24年5月31日 11:23:56的业绩结果,则需要查询两端的时间,加上中间两年七百多天的时间业绩累加,计算量仍然很大
也就是说,如果查询时间区间大于2年,我希望中间的区间是年; 否则如果查询区间大于2个月,我希望中间的区间是月;再然后才是天
另外,我需要基于业绩情况做业绩预测,为了保证精准,我需要统计每天每10分钟内的业绩变化趋势,比如12:08分,我想要预测当天的业绩时,就需要过去的业绩趋势,在每天12:10分的业绩占总业绩的百分比,然后对过去几年的业绩,基于节日情况、是否为周末等做加权处理,简单来说就是,我需要非常频繁地计算每天每10分钟的业绩占当日总业绩的比例
综上,我希望业绩查询的分段区间是基于我的查询区间,可以是年、月、日、小时、甚至是10分钟
基于以上,我想到了使用分段记忆化
每次查询的时候,基于区间大小,自动适配对应的区间
比如查询1月12日到5月12日,则分3段,第一段 1月12日-1月30日,第二段 2月-4月,第3段 5月1日-5月12日
比如查询22年1月12日到24年5月31日,则分3段,第一段 22年1月12日-22年12月32日,第二段 23年,第3段 24年1月1日-24年5月31日
中间那一段都是固定分段,是可以预处理记忆化的,但是:
回到了最初的问题,两端的分区太大了
最终解决方案:递归分段记忆化
我们把两端的分段进行递归,再次分三段,这样递归下去(最小分段为10min),每次查询任意时间区间,其实只有两段10min以内的业绩查询需要过数据库,最终响应时间优化到1s内
代码实现
// 记忆化
class Memorization {
private long totalPrice = 0;//总收入(元)
private long ratePrice = 0; //费率收入(元)
private long rateOrderNum = 0; //费率订单数量
private long salesPrice = 0; //销售总金额(元)
private long salesNum = 0; //销售总笔数(笔)
// 处理数据累加
public void accumulatedData(JSONObject data) {
totalPrice += data.getLong("total_price");
salesPrice += data.getLong("sales_price");
salesNum += data.getLong("sales_num");
ratePrice += data.getLong("rate_price");
rateOrderNum += data.getLong("rate_order_num");
}
// 分段处理数据
public void piecewiseHandleData(long startTime, long endTime, long size) {
long initSize = size;
long currentTime = Instant.now().getEpochSecond(); // 获取当前时间的10位时间戳
// 如果endTime大于当前时间戳,则赋值为当前时间戳
if (endTime > currentTime) {
endTime = currentTime;
}
//如果区间太小 不分段
if (endTime - startTime < initSize) {
accumulatedData(toMemorizationGetIncomeDetail(startTime, endTime));
return;
}
// 阶梯分段
long[] periods = {1, 6, 24, 7 * 24, 30 * 24, 90 * 24, 365 * 24}; // 不同时间段对应的小时数
long durationInSeconds = (endTime - startTime);
for (int i = 0; i < periods.length; i++) {
if (durationInSeconds > periods[i] * 60 * 60 * 3) {
size = periods[i] * 60 * 60;
} else {
break; // 如果不满足条件就跳出循环
}
}
long startDiffTime = startTime + (size - startTime % size);
long endDiffTime = endTime - endTime % size;
//如果分段区间大于初始区间 递归分段
if (size > initSize) {
this.piecewiseHandleData(startTime, startDiffTime, initSize);
this.piecewiseHandleData(endDiffTime, endTime, initSize);
} else {
accumulatedData(toMemorizationGetIncomeDetail(startTime, startDiffTime));
accumulatedData(toMemorizationGetIncomeDetail(endDiffTime, endTime));
}
//处理分段 中段
for (long i = startDiffTime; i < endDiffTime; i += size) {
accumulatedData(toMemorizationGetIncomeDetail(i, i + size));
}
}
//分段处理数据组装
public JSONObject getData(long startTime, long endTime, long size) {
//先分段处理数据
piecewiseHandleData(startTime, endTime, size);
// 数据返回组装
JSONObject json = new JSONObject();
json.put("total_price", totalPrice);
json.put("sales_price", salesPrice);
json.put("sales_num", salesNum);
json.put("rate_price", ratePrice);
json.put("rate_order_num", rateOrderNum);
return json;
}
//获取业绩详情
public JSONObject toMemorizationGetIncomeDetail(long startTime, long endTime) {
//redis存储的key
String redisKey = "gebao-memorization_get_income_detail-" + startTime + '-' + endTime;
//缓存的数据
String redisDataString = redisUtil.get(redisKey);
if (redisDataString != null) {
return JSONObject.parseObject(redisDataString);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
Response res = getIncomeDetailFitterOrderList(startTime, endTime);
// 将Response对象转换为JSON字符串
String jsonString = JSON.toJSONString(res.getData());
redisUtil.set(redisKey, jsonString);
return JSONObject.parseObject(jsonString);
}
}
// 使用方式
Memorization memorization = new Memorization();
return new Response(1, memorization.getData(startTime, endTime, size), "获取成功");
其中size为希望处理的最小分段,如普通查询业绩时,最小分段为1天即可。处理预测模型数据时,最小分段为10min