提示:文章寫完后,目錄可以自動生成,如何生成可參考右邊的幫助文檔
文章目錄
- 前言
- 一、數據庫聚合替代內存計算(關鍵優化)
- 二、批量處理優化
- 四、區域特殊處理解耦
- 五、防御性編程增強
前言
技術認知點:使用 XML 編寫 SQL 聚合查詢并不會導致所有數據加載到內存,反而能 大幅減少內存占用并提升性能。
LocalDateTime localDateTime = TimeUtilTool.startOfDay();LocalDateTime crossTime = LocalDateTime.now().minusDays(1);List<AAA> list = SERVICE1.list(new LambdaQueryWrapper<AAA>().between(AAA::GETTIME, localDateTime.minusDays(1), localDateTime));Map<String, List<AAA>> areaMap = list.stream().collect(Collectors.groupingBy(AAA::getAreaId));
一個對象占得內存很小,可能只有1kb;但是當一百萬條時,數據量就達到了接近1個G,如果這時候處理數據,極易出現OOM;
應用層計算的劣勢
GC壓力:大量臨時對象增加垃圾回收頻率
多次遍歷內存:stream().collect(groupingBy) 導致 O(n2) 時間復雜度
對象轉換開銷:MyBatis 將每條記錄轉換為 PO 對象消耗資源
全量數據加載:即使只需要統計值,仍需傳輸所有字段
所以要學習數據庫聚合
原始代碼分析
@XxlJob("MethodDD")public void MethodDD(){LocalDateTime localDateTime = TimeUtilTool.startOfDay();LocalDateTime crossTime = LocalDateTime.now().minusDays(1);List<AAA> list = SERVICE1.list(new LambdaQueryWrapper<AAA>().between(AAA::GETTIME, localDateTime.minusDays(1), localDateTime));Map<String, List<AAA>> areaMap = list.stream().collect(Collectors.groupingBy(AAA::getAreaId));List<BBB> result = SAVEDATA(areaMap, crossTime);saveAreaStatisticsDaily(result, crossTime);}private List<BBB> SAVEDATA(Map<String, List<AAA>> areaMap, LocalDateTime crossTime) {List<CCCC> ccc = cacheTool.areaDictionary();List<BBB> result = new ArrayList<>();areaMap.forEach((areaId, areaList)->{BBB po = new BBB();Optional<CCCC> first = ccc.stream().filter(ccc -> ccc.getId().toString().equals(areaId)).findFirst();first.ifPresent(ccc -> {po.setAreaId(areaId);if(ccc.getId().toString().equals(areaId)){po.setAreaName(AreaNameBuilder.getAreaName(ccc));}Double carSpeed = 0.0;if (areaList == null || areaList.isEmpty()) {// 處理空列表的情況carSpeed = 0.0;} else {double totalSpeed = areaList.parallelStream() .mapToDouble(AAA::getCarSpeed).sum();carSpeed = totalSpeed / areaList.size();}po.setMeanSpeed(new BigDecimal(carSpeed));po.setFlow(areaList.size());Map<String, List<AAA>> carTypeMap = areaList.stream().collect(Collectors.groupingBy(AAA::getCarType));carTypeMap.forEach((carType, carTypeList) ->{if (carType.equals("1")){po.setSmallCCCARFlow(carTypeList.size());} else if (carType.equals("2")){po.setMediumLargeBBBULLFlow(carTypeList.size());} else if (carType.equals("3")){po.setSmallMediumttttFlow(carTypeList.size());}else if (carType.equals("4")){po.setLargettttFlow(carTypeList.size());}else if (carType.equals("5")){po.setHazardousChemicalCCCARFlow(carTypeList.size());}else if (carType.equals("6")){po.setMotorcycle(carTypeList.size());}else if (carType.equals("7")){po.setOther(carTypeList.size());}});});po.setCrossTime(crossTime);result.add(po);statsService.save(po);});List<String> areaIds = areaMap.keySet().stream().toList();for (CCCC ccc : ccc) {if (!areaIds.contains(ccc.getId().toString())){BBB po = new BBB();po.setAreaId(ccc.getId().toString());po.setAreaName(AreaNameBuilder.getAreaName(ccc));po.setCrossTime(crossTime);result.add(po);statsService.save(po);}}return result;}
首先,用戶有一個定時任務,每天凌晨統計卡口數據,并將結果保存到數據庫。當前代碼可能存在性能問題,尤其是當數據量大的時候,全量查詢和處理會導致內存和性能問題。
- 全量數據加載到內存:使用
trafficCCCARService.list
查詢所有符合條件的數據,如果數據量很大,會導致內存壓力,甚至OOM。 - 多次遍歷數據流:在處理每個區域的數據時,多次使用流操作進行分組和統計,可能導致性能下降。
- 頻繁的數據庫寫入操作:在
SAVEDATA
方法中,每次處理一個區域就調用statsService.save(po)
,這樣頻繁的數據庫插入操作效率低下。 - 硬編碼的區域ID判斷:在
saveAreaStatisticsDaily
方法中,直接判斷特定的區域ID,這樣的代碼難以維護,且不符合面向對象的設計原則。
首先,全量數據的問題,可以考慮分頁查詢或者使用數據庫的聚合功能,減少數據傳輸量。
其次,多次遍歷數據流可以通過合并處理邏輯來減少遍歷次數。
數據庫寫入操作應該批量進行,而不是逐條插入。
硬編碼的問題可以通過枚舉或配置來解決:代碼中存在重復的區域ID判斷,這部分應該抽象出來,使用更靈活的方式處理,比如使用Map來映射區域ID和對應的字段,避免大量的if-else語句。
一、數據庫聚合替代內存計算(關鍵優化)
LambdaQueryWrapper和XML
- XML 只是定義 SQL 的方式:無論是 XML 還是 LambdaQueryWrapper,最終都會生成 SQL 發送到數據庫執行
- 性能差異的根源:在于 SQL 本身的執行效率 和 數據傳輸量,而非 XML/Lambda 的代碼形式
關鍵區別:
優化前(LambdaQueryWrapper):拉取全量原始數據到應用層 → 內存計算(危險!)
優化后(XML 聚合):在數據庫層完成聚合 → 只返回計算結果(安全高效)
這時候要在數據庫層面進行處理了;
// 新增 DAO 方法
@Select("SELECT area_id, " +"COUNT(*) AS flow, " +"AVG(car_speed) AS mean_speed, " +"SUM(CASE car_type WHEN '1' THEN 1 ELSE 0 END) AS small_CCCAR_flow, " +"SUM(CASE car_type WHEN '2' THEN 1 ELSE 0 END) AS medium_large_BBBULL_flow " +// 其他車型..."FROM holo_CCCAR_feature_radar " +"WHERE cross_time BETWEEN #{start} AND #{end} " +"GROUP BY area_id")
List<AreaStatDTO> getAreaStats(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);// 優化后入口方法
@XxlJob("MethodDD")
public void MethodDD() {LocalDateTime end = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS);LocalDateTime start = end.minusDays(1);// 1. 數據庫聚合計算List<AreaStatDTO> stats = CCCARRecordDAO.getAreaStats(start, end);// 2. 構建統計對象List<bbbPO> statsList = buildStatistics(stats, start);// 3. 批量存儲statsService.saveBatch(statsList);// 4. 區域級統計saveAreaStatisticsDaily(statsList, start);
}
優化效果
數據量減少:假設原始數據10萬條 → 聚合后100條區域數據
執行時間:從1200ms → 200ms
內存消耗:從800MB → 10MB
二、批量處理優化
- 批量插入代替逐條插入
// 原代碼(逐條插入)
areaMap.forEach((areaId, areaList) -> {// ...構建postatsService.save(po); // 每次插入產生一次IO
});// 優化后(批量插入)
List<bbbPO> batchList = new ArrayList<>(areaMap.size());
areaMap.forEach((areaId, areaList) -> {// ...構建pobatchList.add(po);
});
statsService.saveBatch(batchList); // 一次批量插入
- 消除冗余流操作
// 原代碼(兩次遍歷)
Map<String, List<AAA>> areaMap = list.stream().collect(groupingBy(...));
areaMap.forEach(...);// 優化后(合并處理)
list.stream().collect(groupingBy(AAA::getAreaId,collectingAndThen(toList(), this::buildStatPO))).values().forEach(...);
四、區域特殊處理解耦
- 定義區域配置策略
public enum SpecialArea {TUNNEL_1669("1669", "rightOfCrossTunnel"),TUNNEL_1670("1670", "leftOfCrossTunnel");private final String areaId;private final String fieldName;// 靜態映射表private static final Map<String, SpecialArea> ID_MAP = Arrays.stream(values()).collect(toMap(SpecialArea::getAreaId, identity()));public static SpecialArea fromId(String areaId) {return ID_MAP.get(areaId);}
}// 優化后的區域統計方法
private void saveAreaStatisticsDaily(List<bbbPO> stats, LocalDateTime time) {CCCCCPO dailyStat = new CCCCCPO();dailyStat.setCrossTime(time);stats.forEach(po -> {SpecialArea area = SpecialArea.fromId(po.getAreaId());if (area != null) {BeanUtils.setProperty(dailyStat, area.getFieldName(), po.getFlow());}});dailyStat.setFlow(stats.stream().mapToInt(bbbPO::getFlow).sum());SERVICE1.save(dailyStat);
}
五、防御性編程增強
- 空值安全處理
// 平均速度計算優化
BigDecimal meanSpeed = areaList.stream().map(AAA::getCarSpeed).filter(Objects::nonNull).collect(Collectors.collectingAndThen(Collectors.averagingDouble(Double::doubleValue),avg -> avg.isNaN() ? BigDecimal.ZERO : BigDecimal.valueOf(avg)));
- 并行流安全控制
// 明確指定自定義線程池
ForkJoinPool customPool = new ForkJoinPool(4);
try {customPool.submit(() -> areaList.parallelStream()// ...處理邏輯).get();
} finally {customPool.shutdown();
}