后端性能优化:从 100ms 到 10ms 的实战记录

3 阅读5分钟

前言

这是一个真实的性能优化案例。

项目背景:订单查询接口,响应时间 100ms,用户体验糟糕。经过 2 周优化,最终降到 10ms。

这篇文章记录完整的优化过程,从定位问题到解决方案,希望对你有帮助。


一、问题定位

1.1 现象描述

接口:/api/orders/{id}
响应时间:100ms
QPS:500
目标:响应时间 < 20ms

1.2 定位工具

工具用途
Arthas方法级耗时分析
SkyWalking链路追踪
MySQL Slow Log慢查询分析
Redis MonitorRedis 命令监控

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] 其他逻辑

问题发现

  1. 数据库查询耗时 50ms(主要瓶颈)
  2. 多次数据库查询(N+1 问题)
  3. 没有使用缓存

二、优化步骤

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        # 连接最大生命周期

三、优化效果对比

优化项优化前优化后提升
数据库查询95ms8ms92% ↓
缓存命中8ms1ms88% ↓
JSON 序列化2ms0.5ms75% ↓
GC 停顿20ms5ms75% ↓
总响应时间100ms10ms90% ↓

四、性能优化检查清单

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: "接口响应时间过高"

总结

性能优化是一个系统工程,需要定位 → 分析 → 优化 → 验证的闭环。

核心思路

  1. 找到瓶颈:用工具定位,不要靠猜
  2. 分层优化:数据库 → 缓存 → 应用 → JVM
  3. 数据驱动:每一步优化都要有数据支撑
  4. 持续监控:优化不是一次性的,要持续关注

记住:过早优化是万恶之源,先保证正确,再追求性能。


💡 互动:你做过哪些性能优化?有什么经验分享?评论区聊聊!