接口响应从2秒优化到200毫秒,我做了这几件事

接手一个老项目,有个列表接口响应时间2秒多,用户体验极差。

花了两天时间优化,最终降到200毫秒以内。把优化过程分享出来。


一、问题背景

接口信息:

  • 功能:订单列表查询
  • 现状:平均响应时间2.3秒
  • 目标:降到500毫秒以内

二、性能分析

2.1 先定位瓶颈

在代码关键位置加日志,看时间花在哪:

日志输出:


查询订单耗时: 1200ms ← 大头!
查询用户耗时: 450ms ← 也很慢
查询商品耗时: 380ms
数据组装耗时: 50ms
总耗时: 2080ms

找到瓶颈了:**SQL查询 + 多次远程调用**

---

## 三、优化措施

### 优化1:SQL索引优化

**原SQL:**
```sql
SELECT * FROM orders 
WHERE user_id = 12345 
  AND status IN (1, 2, 3)
ORDER BY create_time DESC
LIMIT 0, 20;

EXPLAIN分析:

type: ALL(全表扫描!)
rows: 5000000

优化:添加联合索引

ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);

效果:1200ms → 50ms


优化2:避免N+1查询

原代码(有问题):

// 循环里查询,典型的N+1问题
for (Order order : orders) {
    User user = userService.getUser(order.getUserId());
    order.setUserName(user.getName());
}

优化:批量查询

// 收集所有ID,一次性查询
List<Long> userIds = orders.stream()
    .map(Order::getUserId)
    .distinct()
    .collect(toList());

Map<Long, User> userMap = userService.batchGetUsers(userIds);

效果:N次查询 → 1次查询


优化3:并行查询

用户信息和商品信息可以并行查:

原代码(串行):

// 总耗时 = 450 + 380 = 830ms
Map<Long, User> userMap = userService.batchGetUsers(userIds);
Map<Long, Product> productMap = productService.batchGetProducts(productIds);

优化:并行执行

// 总耗时 = max(450, 380) = 450ms
CompletableFuture<Map<Long, User>> userFuture = 
    CompletableFuture.supplyAsync(() -> userService.batchGetUsers(userIds));

CompletableFuture<Map<Long, Product>> productFuture = 
    CompletableFuture.supplyAsync(() -> productService.batchGetProducts(productIds));

Map<Long, User> userMap = userFuture.get();
Map<Long, Product> productMap = productFuture.get();

效果:830ms → 450ms


优化4:添加缓存

用户和商品信息不经常变,加缓存:

public Map<Long, User> batchGetUsers(List<Long> userIds) {
    Map<Long, User> result = new HashMap<>();
    List<Long> missIds = new ArrayList<>();
    
    // 先查缓存
    for (Long id : userIds) {
        User cached = cache.get("user:" + id);
        if (cached != null) {
            result.put(id, cached);
        } else {
            missIds.add(id);
        }
    }
    
    // 查数据库
    if (!missIds.isEmpty()) {
        List<User> users = userMapper.selectByIds(missIds);
        for (User user : users) {
            result.put(user.getId(), user);
            cache.put("user:" + user.getId(), user, 30, TimeUnit.MINUTES);
        }
    }
    
    return result;
}

效果:450ms → 50ms(缓存命中时)


四、优化效果

优化项 优化前 优化后 提升
SQL查询 1200ms 50ms 24倍
用户查询 450ms 50ms 9倍
商品查询 380ms 40ms 9.5倍
总计 2080ms 190ms 11倍

关键指标变化:

P50延迟:1800ms → 150ms
P99延迟:3200ms → 280ms
QPS承载:45 → 380
错误率:2.3% → 0.1%

五、深入分析:为什么这些优化有效

5.1 索引优化的本质

B+树索引时间复杂度:O(log n)
全表扫描时间复杂度:O(n)

500万数据:
- 全表扫描:500万次比较
- B+树索引:约23次比较(log₂ 5000000 ≈ 22.3)

这就是为什么索引能提升几千倍

5.2 并行化的理论基础

串行执行:T = T1 + T2 + T3
并行执行:T = max(T1, T2, T3)

Amdahl定律:
加速比 = 1 / (S + P/N)
S = 串行部分占比
P = 可并行部分占比
N = 并行度

实际中,IO密集型任务并行收益更高

5.3 缓存命中率与性能

假设:
- DB查询:50ms
- Redis查询:2ms
- 缓存命中率:90%

平均延迟 = 0.9 × 2ms + 0.1 × 50ms = 6.8ms

缓存命中率每提升10%,延迟降低约5ms

六、生产环境注意事项

6.1 CompletableFuture线程池配置

// 不要使用默认的ForkJoinPool.commonPool()
// 生产环境应自定义线程池
private static final ExecutorService ASYNC_POOL = new ThreadPoolExecutor(
    10, 50, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(200),
    new ThreadFactoryBuilder().setNameFormat("async-query-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

CompletableFuture.supplyAsync(() -> userService.query(), ASYNC_POOL);

6.2 缓存一致性处理

// 更新数据时,先更新DB,再删除缓存
@Transactional
public void updateUser(User user) {
    userMapper.update(user);
    cache.delete("user:" + user.getId());  // 删除而非更新
}

6.3 监控埋点

// 使用Micrometer记录关键指标
Timer.Sample sample = Timer.start(registry);
try {
    return doQuery();
} finally {
    sample.stop(registry.timer("api.query.time", "method", "getOrderList"));
}

八、优化总结

8.1 优化清单

✅ SQL索引优化(最重要)
✅ 避免N+1查询(批量查询)
✅ 并行查询(CompletableFuture)
✅ 添加缓存(Redis/本地缓存)
✅ 减少数据传输(按需查字段)

8.2 优化优先级

1. 先看SQL(80%的慢都是SQL问题)
2. 减少调用次数(N+1 → 批量)
3. 并行化(串行 → 并行)
4. 加缓存(热点数据)

九、测试环境联调

性能问题在本地很难复现,需要连测试环境的真实数据。

我用星空组网把本地和测试环境连起来,直接连测试库跑SQL:

# 直连测试环境数据库
mysql -h 192.168.188.10 -u dev -p testdb

本地IDEA直接断点调试,看每一步耗时,比在服务器上加日志效率高多了。


十、总结

接口性能优化的核心思路:

1. 定位瓶颈(不要盲目优化)
2. SQL先行(80%的问题在SQL)
3. 减少IO(批量、缓存、并行)
4. 量化效果(优化前后对比)

记住:没有测量就没有优化。

这次从2秒优化到200毫秒,用户反馈"秒开",成就感满满。

有问题评论区交流~


posted @ 2025-12-05 10:44  花宝宝  阅读(3)  评论(0)    收藏  举报