找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

460

积分

0

好友

58

主题
发表于 5 小时前 | 查看: 1| 回复: 0

很多性能问题并非源于技术选型不当,而是在编码实现时未能充分考虑数据规模增长带来的影响。例如,在数据处理逻辑中不经意的使用低效循环,当数据量从几百条激增到数万条时,系统瓶颈便会立刻显现。

下面这段代码就是一个典型的例子,它能够正确运行,但其性能和可维护性存在显著的优化空间:

public void calculateCmsResult() {
    // 获取绩效列表
    List<CmsPerformanceCalculateVo> performanceVos = cmsPerformanceService.queryCalculateList();
    if(performanceVos == null || performanceVos.isEmpty()){
        return;
    }

    // 销售人员id
    Set<Long> salesOwnerIds = performanceVos.stream()
        .map(CmsPerformanceCalculateVo::getCurrentSalesOwnerId)
        .collect(Collectors.toSet());

    List<CmsResult> resultList = new ArrayList<>();
    for (Long salesOwnerId : salesOwnerIds){
        CmsResult cmsResult = new CmsResult();
        cmsResult.setOperatorUserId(salesOwnerId);
        // 获取上个月第一天
        cmsResult.setPerformanceMonth(DateUtils.getLastMonthFirstDay());

        // 总销售额
        BigDecimal totalSales = BigDecimal.ZERO;
        // 新品销售额
        BigDecimal newProductSales = BigDecimal.ZERO;
        // 老品毛利额
        BigDecimal totalProfit = BigDecimal.ZERO;

        // 总毛利额
        BigDecimal totalProfit = BigDecimal.ZERO;
        // 新品毛利额
        BigDecimal newProductSales = BigDecimal.ZERO;
        // 老品毛利额
        BigDecimal oldProductProfit = BigDecimal.ZERO;

        for (CmsPerformanceCalculateVo performanceVo : performanceVos) {
            if(performanceVo.getSalesOwnerId().equals(salesOwnerId)){
                totalSales = totalSales.add(performanceVo.getSalesAmount());
                totalProfit = totalProfit.add(performanceVo.getGrossProfit());

                // 新品
                if(performanceVo.getProductType().equals(CmsProductType.NEW_PROD.getDbValue())){
                    newProductSales = newProductSales.add(performanceVo.getSalesAmount());
                    newProductProfit = newProductProfit.add(performanceVo.getGrossProfit());
                }

                // 老品
                if(performanceVo.getProductType().equals(CmsProductType.OLD_PROD.getDbValue())){
                    oldProductSales = oldProductSales.add(performanceVo.getSalesAmount());
                    oldProductProfit = oldProductProfit.add(performanceVo.getGrossProfit());
                }
            }
        }

        cmsResult.setTotalSales(totalSales);
        cmsResult.setNewProductSales(newProductSales);
        cmsResult.setOldProductSales(oldProductSales);
        cmsResult.setTotalProfit(totalProfit);
        cmsResult.setNewProductProfit(newProductProfit);
        cmsResult.setOldProductProfit(oldProductProfit);
        resultList.add(cmsResult);
    }
}

一、当前代码存在的核心问题

1. 时间复杂度过高(O(n²))

问题的核心在于这段嵌套循环:

for (Long salesOwnerId : salesOwnerIds) {
    for (CmsPerformanceCalculateVo performanceVo : performanceVos) {
        if (performanceVo.getSalesOwnerId().equals(salesOwnerId)) {
            ...
        }
    }
}
  • 假设:销售人员数量为 M,绩效数据条数为 N。
  • 实际复杂度M × N。当 M 和 N 较大时,这将是指数级的性能消耗。
  • 后果:一旦数据量增长到几万行,执行此逻辑的定时任务或接口很可能被直接拖垮,这正是典型的算法优化问题。

2. 同一个列表被重复遍历

代码中,performanceVos 这个完整的列表被遍历了 salesOwnerIds.size() 次。这是最主要的性能浪费点,极大地增加了不必要的计算开销。

3. if 判断与 BigDecimal 累加逻辑重复

对于新产品、老产品的销售额和毛利额计算,代码结构几乎完全一致,只是判断条件不同。这种重复不仅导致代码冗长,更糟糕的是,当未来需要增加新的产品类型时,你不得不进行大量的复制粘贴,极易出错且难以维护。

4. productType 的 equals 写法存在风险

代码直接使用 performanceVo.getProductType().equals(...) 进行判断。如果 getProductType() 返回 null,将会直接抛出 NullPointerException,导致程序异常中断,健壮性不足。

二、优化核心思路与原则

解决上述问题的关键在于转变数据处理模式:变“逐人遍历全表”为“一次分组,各自汇总”

核心优化原则:一次遍历完成数据分组,后续每个分组独立处理,避免全局扫描。

优化步骤拆解

  1. 先按 salesOwnerId 利用 Map 进行分组。
  2. 每个销售人员仅遍历属于他自己的那部分数据。
  3. 使用更安全的枚举和 switch 语句处理产品类型,避免魔法值和NPE。
  4. 抽象公共累加逻辑,减少重复代码。

三、优化后的实现方案

1. 先分组(关键一步)

利用 Java 8 的 Stream API 可以非常优雅地完成分组操作:

Map<Long, List<CmsPerformanceCalculateVo>> groupMap =
        performanceVos.stream()
            .collect(Collectors.groupingBy(CmsPerformanceCalculateVo::getSalesOwnerId));

这一步将时间复杂度从 O(M×N) 降低到了 O(N),是性能提升的核心。

2. 主逻辑重构(单次遍历分组数据)

优化后的完整方法如下:

public void calculateCmsResult() {

    List<CmsPerformanceCalculateVo> performanceVos =
            cmsPerformanceService.queryCalculateList();
    if (CollectionUtils.isEmpty(performanceVos)) {
        return;
    }

    // 分组:关键优化,将O(n²)降为O(n)
    Map<Long, List<CmsPerformanceCalculateVo>> groupMap =
            performanceVos.stream()
                .collect(Collectors.groupingBy(CmsPerformanceCalculateVo::getSalesOwnerId));

    List<CmsResult> resultList = new ArrayList<>();

    for (Map.Entry<Long, List<CmsPerformanceCalculateVo>> entry : groupMap.entrySet()) {

        Long salesOwnerId = entry.getKey();
        List<CmsPerformanceCalculateVo> list = entry.getValue(); // 仅处理该人员的数据

        CmsResult cmsResult = new CmsResult();
        cmsResult.setOperatorUserId(salesOwnerId);
        cmsResult.setPerformanceMonth(DateUtils.getLastMonthFirstDay());

        BigDecimal totalSales = BigDecimal.ZERO;
        BigDecimal totalProfit = BigDecimal.ZERO;

        BigDecimal newSales = BigDecimal.ZERO;
        BigDecimal newProfit = BigDecimal.ZERO;

        BigDecimal oldSales = BigDecimal.ZERO;
        BigDecimal oldProfit = BigDecimal.ZERO;

        for (CmsPerformanceCalculateVo vo : list) { // 内层循环数据量大幅减少

            totalSales = totalSales.add(vo.getSalesAmount());
            totalProfit = totalProfit.add(vo.getGrossProfit());

            Integer productType = vo.getProductType();
            if (productType == null) { // 增加空值判断,避免NPE
                continue;
            }

            switch (CmsProductType.of(productType)) { // 使用枚举,更清晰
                case NEW_PROD:
                    newSales = newSales.add(vo.getSalesAmount());
                    newProfit = newProfit.add(vo.getGrossProfit());
                    break;
                case OLD_PROD:
                    oldSales = oldSales.add(vo.getSalesAmount());
                    oldProfit = oldProfit.add(vo.getGrossProfit());
                    break;
                default:
                    break;
            }
        }

        cmsResult.setTotalSales(totalSales);
        cmsResult.setTotalProfit(totalProfit);
        cmsResult.setNewProductSales(newSales);
        cmsResult.setNewProductProfit(newProfit);
        cmsResult.setOldProductSales(oldSales);
        cmsResult.setOldProductProfit(oldProfit);

        resultList.add(cmsResult);
    }
}

四、进阶优化建议

1. 完善枚举,提升代码可读性与健壮性

定义一个包含静态工厂方法的枚举,彻底消除魔法数字,并安全地处理未知类型。

public enum CmsProductType {
    NEW_PROD(1),
    OLD_PROD(2),
    CONTINUE_PROD(3);

    private final Integer dbValue;

    public static CmsProductType of(Integer value) {
        if (value == null) {
            return null;
        }
        for (CmsProductType type : values()) {
            if (type.dbValue.equals(value)) {
                return type;
            }
        }
        return null; // 或定义一个 UNKNOWN 枚举实例
    }
}

2. 抽取工具方法,减少重复代码

可以封装一个安全的 BigDecimal 累加方法,避免每次累加时都进行空值判断。

private BigDecimal add(BigDecimal a, BigDecimal b) {
    return a.add(b == null ? BigDecimal.ZERO : b);
}

总结:当数据量较小时,双层循环的缺点往往被忽视;但当业务增长,数据量膨胀到数万甚至数十万级别时,其带来的性能瓶颈将是灾难性的。通过预先使用 Map 进行分组,将算法时间复杂度从 O(n²) 优化为 O(n),是处理此类业务数据处理场景中最有效、最经典的优化手段之一。在云栈社区后端 & 架构板块,你可以找到更多关于高性能编程和系统设计的深度讨论与案例分享。




上一篇:Java线程安全有界阻塞队列源码解析:ArrayBlockingQueue
下一篇:掌握CPU模拟器Unicorn框架:从基础到实战进阶指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 16:50 , Processed in 0.230811 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表