很多性能问题并非源于技术选型不当,而是在编码实现时未能充分考虑数据规模增长带来的影响。例如,在数据处理逻辑中不经意的使用低效循环,当数据量从几百条激增到数万条时,系统瓶颈便会立刻显现。
下面这段代码就是一个典型的例子,它能够正确运行,但其性能和可维护性存在显著的优化空间:
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,导致程序异常中断,健壮性不足。
二、优化核心思路与原则
解决上述问题的关键在于转变数据处理模式:变“逐人遍历全表”为“一次分组,各自汇总”。
核心优化原则:一次遍历完成数据分组,后续每个分组独立处理,避免全局扫描。
优化步骤拆解:
- 先按
salesOwnerId 利用 Map 进行分组。
- 每个销售人员仅遍历属于他自己的那部分数据。
- 使用更安全的枚举和
switch 语句处理产品类型,避免魔法值和NPE。
- 抽象公共累加逻辑,减少重复代码。
三、优化后的实现方案
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),是处理此类业务数据处理场景中最有效、最经典的优化手段之一。在云栈社区的后端 & 架构板块,你可以找到更多关于高性能编程和系统设计的深度讨论与案例分享。