接口响应从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毫秒,用户反馈"秒开",成就感满满。
有问题评论区交流~

浙公网安备 33010602011771号