💥用递归分段记忆化,我把业绩统计的响应时间从100秒优化到了1秒

337 阅读5分钟

image.png

背景说明

最近在实现通过聊天机器人查询业绩时,当查询一年以上的业绩情况,需要处理百万级的数据量,导致响应时间极慢,一般在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