前言
这是一个真实的性能优化案例。
项目背景:订单查询接口,响应时间 100ms,用户体验糟糕。经过 2 周优化,最终降到 10ms。
这篇文章记录完整的优化过程,从定位问题到解决方案,希望对你有帮助。
一、问题定位
1.1 现象描述
接口:/api/orders/{id}
响应时间:100ms
QPS:500
目标:响应时间 < 20ms
1.2 定位工具
| 工具 | 用途 |
|---|---|
| Arthas | 方法级耗时分析 |
| SkyWalking | 链路追踪 |
| MySQL Slow Log | 慢查询分析 |
| Redis Monitor | Redis 命令监控 |
1.3 问题分析
通过 Arthas trace 发现:
`---ts=2024-03-01 10:00:00;thread_name=http-nio-8080-exec-1
`---[100.23ms] com.example.service.OrderService:getOrder()
+---[50.12ms] com.example.mapper.OrderMapper:selectById()
+---[30.45ms] com.example.mapper.UserMapper:selectById()
+---[15.33ms] com.example.mapper.ProductMapper:selectById()
`---[4.33ms] 其他逻辑
问题发现:
- 数据库查询耗时 50ms(主要瓶颈)
- 多次数据库查询(N+1 问题)
- 没有使用缓存
二、优化步骤
2.1 数据库优化
问题 1:慢查询
原 SQL:
SELECT * FROM orders WHERE id = 123;
-- 耗时:50ms
分析:
EXPLAIN SELECT * FROM orders WHERE id = 123;
-- 发现:全表扫描,没有走索引
优化方案:
-- 添加主键索引(如果 id 不是主键)
ALTER TABLE orders ADD PRIMARY KEY (id);
-- 或者添加唯一索引
CREATE UNIQUE INDEX idx_order_id ON orders(id);
效果:查询时间从 50ms 降到 5ms。
问题 2:N+1 查询
原代码:
public OrderDTO getOrder(Long id) {
Order order = orderMapper.selectById(id); // 50ms
User user = userMapper.selectById(order.getUserId()); // 30ms
Product product = productMapper.selectById(order.getProductId()); // 15ms
// 总耗时:95ms
return buildDTO(order, user, product);
}
优化方案 1:JOIN 查询
public OrderDTO getOrder(Long id) {
return orderMapper.selectOrderWithDetails(id);
}
// Mapper
@Select("SELECT o.*, u.name as user_name, p.name as product_name " +
"FROM orders o " +
"LEFT JOIN users u ON o.user_id = u.id " +
"LEFT JOIN products p ON o.product_id = p.id " +
"WHERE o.id = #{id}")
OrderDTO selectOrderWithDetails(Long id);
效果:从 3 次查询变成 1 次,耗时从 95ms 降到 8ms。
优化方案 2:批量查询
public List<OrderDTO> getOrders(List<Long> ids) {
List<Order> orders = orderMapper.selectBatchIds(ids);
List<Long> userIds = orders.stream().map(Order::getUserId).distinct().toList();
List<User> users = userMapper.selectBatchIds(userIds);
// 组装数据
return buildDTOs(orders, users);
}
2.2 缓存优化
问题:没有缓存
优化方案:Redis 缓存
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public OrderDTO getOrder(Long id) {
String key = "order:" + id;
// 1. 查缓存
OrderDTO dto = (OrderDTO) redisTemplate.opsForValue().get(key);
if (dto != null) {
return dto;
}
// 2. 查数据库
dto = orderMapper.selectOrderWithDetails(id);
// 3. 写缓存
redisTemplate.opsForValue().set(key, dto, 30, TimeUnit.MINUTES);
return dto;
}
}
效果:缓存命中时,响应时间从 8ms 降到 1ms。
缓存策略优化
// 本地缓存 + Redis 双层缓存
@Component
public class OrderCache {
private final LoadingCache<Long, OrderDTO> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(id -> loadFromRedis(id));
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public OrderDTO get(Long id) {
return localCache.get(id);
}
private OrderDTO loadFromRedis(Long id) {
OrderDTO dto = (OrderDTO) redisTemplate.opsForValue().get("order:" + id);
if (dto != null) {
return dto;
}
dto = loadFromDatabase(id);
redisTemplate.opsForValue().set("order:" + id, dto, 30, TimeUnit.MINUTES);
return dto;
}
}
2.3 代码优化
问题 1:JSON 序列化慢
原代码:
return JSON.toJSONString(order); // FastJSON,较慢
优化方案:
return Jackson.toJson(order); // Jackson,更快
// 或
return Gson.toJson(order); // Gson,也不错
效果:序列化时间从 2ms 降到 0.5ms。
问题 2:对象创建过多
原代码:
List<OrderDTO> result = new ArrayList<>();
for (Order order : orders) {
OrderDTO dto = new OrderDTO();
dto.setId(order.getId());
dto.setUserName(order.getUserName());
// ... 很多 set
result.add(dto);
}
优化方案:
// 使用 MapStruct 自动映射
@Mapper
public interface OrderMapper {
OrderDTO toDTO(Order order);
List<OrderDTO> toDTOList(List<Order> orders);
}
// 使用
List<OrderDTO> result = orderMapper.toDTOList(orders);
2.4 JVM 优化
GC 调优
原配置:
java -Xms256m -Xmx256m -jar app.jar
优化配置:
java -Xms1g -Xmx1g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=50 \
-XX:+ParallelRefProcEnabled \
-jar app.jar
效果:GC 停顿从 20ms 降到 5ms。
2.5 网络优化
问题:数据库连接池配置不当
原配置:
spring:
datasource:
hikari:
maximum-pool-size: 10
优化配置:
spring:
datasource:
hikari:
maximum-pool-size: 50 # 最大连接数
minimum-idle: 10 # 最小空闲连接
idle-timeout: 300000 # 空闲超时
connection-timeout: 5000 # 连接超时
max-lifetime: 1800000 # 连接最大生命周期
三、优化效果对比
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 数据库查询 | 95ms | 8ms | 92% ↓ |
| 缓存命中 | 8ms | 1ms | 88% ↓ |
| JSON 序列化 | 2ms | 0.5ms | 75% ↓ |
| GC 停顿 | 20ms | 5ms | 75% ↓ |
| 总响应时间 | 100ms | 10ms | 90% ↓ |
四、性能优化检查清单
4.1 数据库层
- 慢查询是否加了索引
- 是否有 N+1 查询
- 连接池配置是否合理
- 是否用了分库分表
- 是否有锁等待
4.2 缓存层
- 热点数据是否缓存
- 缓存过期时间是否合理
- 是否有缓存穿透/击穿/雪崩
- 是否用了本地缓存
4.3 应用层
- 是否有慢方法
- 是否有锁竞争
- 是否有线程池阻塞
- 是否有内存泄漏
4.4 JVM 层
- 堆内存配置是否合理
- GC 算法是否合适
- 是否有频繁 Full GC
- 是否有内存泄漏
五、监控建设
5.1 关键指标
应用层:
- 接口响应时间(P50、P95、P99)
- QPS
- 错误率
数据库层:
- 慢查询数量
- 连接池使用率
- 锁等待时间
缓存层:
- 缓存命中率
- Redis 响应时间
5.2 告警规则
# Prometheus 告警规则
groups:
- name: performance
rules:
- alert: HighLatency
expr: http_request_duration_seconds{quantile="0.99"} > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "接口响应时间过高"
总结
性能优化是一个系统工程,需要定位 → 分析 → 优化 → 验证的闭环。
核心思路:
- 找到瓶颈:用工具定位,不要靠猜
- 分层优化:数据库 → 缓存 → 应用 → JVM
- 数据驱动:每一步优化都要有数据支撑
- 持续监控:优化不是一次性的,要持续关注
记住:过早优化是万恶之源,先保证正确,再追求性能。
💡 互动:你做过哪些性能优化?有什么经验分享?评论区聊聊!