最近在Review团队代码时,发现了不少值得分享的性能问题和优化实践。作为有五年Java开发经验的工程师,我整理了几个典型案例,这些都是在实际项目中容易忽视但影响深远的细节。下面我会逐一分析问题本质,并给出经过验证的优化方案。
在SupplierServiceImpl.getSupplierVoById方法中,当数据量较大时出现了数据截断问题。表面看是分页参数设置不当,但深入分析后发现这是典型的"全量查询+内存分页"反模式。
关键问题点:开发者在处理分页时,先查询全部数据到内存,再通过subList进行分页操作。当数据量达到10万级别时,不仅消耗大量内存,还会导致GC频繁,最终引发OOM。
正确的分页处理应该遵循"数据库分页"原则:
java复制// 错误做法:内存分页
List<Supplier> allData = supplierMapper.selectAll();
List<Supplier> pageData = allData.subList(start, end);
// 正确做法:SQL分页
PageHelper.startPage(pageNum, pageSize);
List<Supplier> pageData = supplierMapper.selectByExample(example);
优化要点:
limit offset, size条件sql复制SELECT * FROM supplier WHERE id > last_id ORDER BY id LIMIT 100
在统计查询中,发现一个耗时3.8秒的慢SQL,其IN子句包含717个ID,返回13850行数据。但最终业务只需要其中一条记录。
sql复制-- 原始低效SQL
SELECT * FROM order_detail
WHERE supplier_id IN (id1,id2,...,id717)
问题本质:大量IN查询导致全表扫描 + 不必要的网络传输
分步优化方案:
sql复制SELECT SUM(amount) FROM order_detail
WHERE supplier_id = ? AND create_time BETWEEN ? AND ?
| 方案 | 执行时间 | 网络传输 | 内存占用 |
|---|---|---|---|
| 原始方案 | 3800ms | 1.2MB | 45MB |
| 优化方案 | 23ms | 0.5KB | 1MB |
在ProjectInventoryInServiceImpl.importProjectInDetail方法中,存在循环内单条查询的问题:
java复制for(ImportItem item : items) {
// 每次循环都执行一次查询
Inventory inventory = inventoryMapper.selectById(item.getId());
process(inventory);
}
当导入200条数据时,接口耗时超过100秒。
优化原则:将N+1查询转换为1+1查询
java复制// 先批量获取所有ID
List<Long> ids = items.stream().map(ImportItem::getId).collect(toList());
// 一次批量查询
Map<Long, Inventory> inventoryMap = inventoryMapper.selectBatchIds(ids)
.stream().collect(toMap(Inventory::getId, Function.identity()));
// 内存处理
for(ImportItem item : items) {
process(inventoryMap.get(item.getId()));
}
性能提升:
使用Double直接转为BigDecimal时出现精度丢失:
java复制double d = 1.4;
BigDecimal bd = new BigDecimal(d); // 实际值:1.3999999...
推荐方案:
java复制// 方案1:使用字符串构造
BigDecimal bd1 = new BigDecimal("1.4");
// 方案2:使用valueOf方法(内部调用Double.toString)
BigDecimal bd2 = BigDecimal.valueOf(1.4);
原理说明:
new BigDecimal(double)会先转为IEEE754二进制表示,再转为十进制,导致精度损失valueOf方法通过字符串中转,保留了原始精度在统计报表代码中,多次使用Stream.filter进行数据筛选:
java复制List<Order> foodOrders = orders.stream()
.filter(o -> "food".equals(o.getType()))
.collect(toList());
问题点:
java复制// 一次性分组
Map<String, List<Order>> orderMap = orders.stream()
.collect(groupingBy(Order::getType));
// 直接获取分类数据
List<Order> foodOrders = orderMap.get("food");
性能对比(万级数据):
| 操作方式 | 执行时间 |
|---|---|
| 多次filter | 45ms |
| 分组Map | 8ms |
默认创建的ArrayList初始容量为10,当元素超过容量时会触发扩容(1.5倍增长)。频繁扩容会导致:
java复制// 已知最终大小
List<DailyGoodsCost> list = new ArrayList<>(detailVos.size());
// 预估大小(稍大比小好)
List<DailyGoodsCost> list = new ArrayList<>(estimatedSize + 10);
扩容次数对比:
| 元素数量 | 默认扩容次数 | 预分配后 |
|---|---|---|
| 100 | 5 | 0 |
| 10000 | 14 | 0 |
原始代码中存在两次独立排序:
java复制// 第一次排序
list.sort(Comparator.comparing(Item::getSchoolId));
// 第二次排序
list.sort(Comparator.comparing(Item::getDate).reversed());
java复制list.sort(Comparator.comparing(Item::getSchoolId)
.thenComparing(Comparator.comparing(Item::getDate).reversed()));
优势分析:
发现DTO类中存在下划线命名字段:
java复制public class ProviderResultDataDto {
private String provider_name; // 不规范
private String providerCode; // 推荐
}
规范建议:
在优化过程中需要注意:
经过这次系统的代码Review,我总结了以下几点经验:
性能问题往往隐藏在看似无害的代码中:比如一个普通的for循环,当数据量增长后就会成为瓶颈
数据库操作是主要性能热点:N+1查询、全表扫描、不合理的JOIN等需要重点关注
测量比猜测更重要:优化前先用Arthas或JMH进行性能分析,找到真正的热点
代码是写给人看的:在优化性能的同时,要保持良好的可读性和可维护性
建立团队Code Review机制:定期进行代码互查,分享最佳实践
在实际项目中,我建议将这类优化点整理成Checklist,纳入团队的代码审查标准。同时,对于核心业务代码,应该建立性能基准测试,防止优化过程中引入性能回退。