接口性能优化:从 5 秒到 200ms 的 7 步实战调优全记录

引言

线上告警:订单查询接口 P99 飙到 5 秒,用户疯狂投诉。

打开监控一看,QPS 不高,机器负载也不高,但就是慢。典型的"代码写得烂"型性能问题。

本文记录这个接口从 5 秒优化到 200ms 的全过程,7 步走,每一步都有前后对比数据。不是理论,是真实线上案例。


零、问题接口

0.1 接口描述

GET /api/orders/{orderId}

功能:根据订单 ID 查询订单详情
返回:订单基本信息 + 用户信息 + 商品列表 + 物流信息 + 优惠信息

0.2 初始代码(慢得离谱的版本)

@Service
public class OrderQueryService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private LogisticsMapper logisticsMapper;
    @Autowired
    private CouponMapper couponMapper;

    public OrderDetailVO queryOrder(Long orderId) {
        // 1. 查订单
        Order order = orderMapper.selectById(orderId);

        // 2. 查用户
        User user = userMapper.selectById(order.getUserId());

        // 3. 查商品列表(N+1 问题)
        List<OrderItem> items = orderMapper.selectOrderItems(orderId);
        List<Product> products = new ArrayList<>();
        for (OrderItem item : items) {
            Product product = productMapper.selectById(item.getProductId());
            products.add(product);
        }

        // 4. 查物流
        Logistics logistics = logisticsMapper.selectByOrderId(orderId);

        // 5. 查优惠
        Coupon coupon = couponMapper.selectByOrderId(orderId);

        // 6. 组装返回
        return buildVO(order, user, products, logistics, coupon);
    }
}

0.3 初始性能数据

压测工具:JMeter,50 并发,持续 1 分钟

平均响应时间:5120ms
P95:6800ms
P99:8200ms
QPS:9.5

5 秒一个请求,用户不投诉才怪。

0.4 优化路线图

Step 1: 加索引          5120ms → 1200ms   (数据库层)
Step 2: 减少联表        1200ms → 800ms    (SQL 优化)
Step 3: Redis 缓存      800ms → 50ms     (缓存层)
Step 4: 并行查询        50ms → 30ms      (多线程)
Step 5: 字段裁剪        30ms → 25ms      (减少数据传输)
Step 6: 批量合并        25ms → 20ms      (消灭 N+1)
Step 7: 异步化          20ms → 200ms*    (非核心数据异步)

* 最终接口返回核心数据 200ms,非核心数据异步加载

Step 1:加索引(5120ms → 1200ms)

1.1 定位慢 SQL

开启慢查询日志:

-- MySQL 慢查询配置
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录

抓到几条慢 SQL:

-- 慢 SQL 1:查订单(全表扫描)
SELECT * FROM orders WHERE order_id = 12345;
-- 扫描行数:280 万行(全表扫描)
-- 耗时:2.8s

-- 慢 SQL 2:查物流(全表扫描)
SELECT * FROM logistics WHERE order_id = 12345;
-- 扫描行数:150 万行
-- 耗时:1.5s

-- 慢 SQL 3:查优惠(全表扫描)
SELECT * FROM coupon WHERE order_id = 12345;
-- 扫描行数:80 万行
-- 耗时:0.8s

1.2 用 EXPLAIN 分析

EXPLAIN SELECT * FROM orders WHERE order_id = 12345;
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | orders | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 2800000 |    10.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+

type=ALL,全表扫描,没走索引。

1.3 加索引

-- 订单表主键索引(如果没有)
ALTER TABLE orders ADD PRIMARY KEY (order_id);

-- 物流表:order_id 加索引
ALTER TABLE logistics ADD INDEX idx_order_id (order_id);

-- 优惠表:order_id 加索引
ALTER TABLE coupon ADD INDEX idx_order_id (order_id);

-- 订单商品表:order_id 加索引
ALTER TABLE order_item ADD INDEX idx_order_id (order_id);

1.4 验证效果

EXPLAIN SELECT * FROM orders WHERE order_id = 12345;
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | type   | key   | key_len| ref          | rows    | Extra   |
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | const  | PRIMARY| 8     | const         | 1       | NULL    |
+----+--------+-------+-------+---------------+---------+---------+-------+------+-------+

type=const,走主键索引,扫描 1 行。

1.5 性能对比

优化前:5120ms
优化后:1200ms
提升:76%
SQL优化前优化后
SELECT * FROM orders2800ms2ms
SELECT * FROM logistics1500ms3ms
SELECT * FROM coupon800ms2ms
其他20ms1193ms

教训:第一步永远是看有没有索引。80% 的慢查询都是没加索引。


Step 2:减少联表(1200ms → 800ms)

2.1 发现联表过多的 SQL

订单详情查询用了 4 表联查:

SELECT
    o.*,
    u.username,
    u.phone,
    p.product_name,
    p.price,
    l.logistics_status,
    l.tracking_no
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_item oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
LEFT JOIN logistics l ON o.order_id = l.order_id
WHERE o.order_id = 12345;

4 张表 JOIN,执行计划复杂,临时表 + 文件排序。

2.2 优化策略:拆分联表

把一个大 JOIN 拆成多个简单查询,让每个查询都能走索引:

// 优化后:拆成 4 个独立查询,每个都走索引
public OrderDetailVO queryOrder(Long orderId) {
    // 1. 查订单(走主键)
    Order order = orderMapper.selectById(orderId);

    // 2. 查用户(走主键)
    User user = userMapper.selectById(order.getUserId());

    // 3. 查商品列表(走 idx_order_id)
    List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);

    // 4. 查物流(走 idx_order_id)
    Logistics logistics = logisticsMapper.selectByOrderId(orderId);

    return buildVO(order, user, items, logistics);
}

2.3 为什么拆分反而更快

维度4 表 JOIN拆分查询
索引利用JOIN 后执行计划复杂,可能放弃索引每个查询独立走索引
锁竞争大 JOIN 锁多张表独立查询锁粒度小
缓存友好一个表变更,整个 JOIN 缓存失效独立缓存
可扩展无法并行可改为并行查询(Step 4)

注意:拆分不是绝对的。如果 JOIN 的表都很小(几百行),JOIN 反而更快(减少网络往返)。大表才拆。

2.4 性能对比

优化前:1200ms
优化后:800ms
提升:33%

Step 3:Redis 缓存(800ms → 50ms)

3.1 加缓存策略

订单数据读多写少,天然适合缓存。用"旁路缓存"模式:

@Service
public class OrderQueryService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private OrderMapper orderMapper;

    private static final String ORDER_CACHE_KEY = "order:detail:";

    public OrderDetailVO queryOrder(Long orderId) {
        String cacheKey = ORDER_CACHE_KEY + orderId;

        // 1. 先查缓存
        OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;  // 缓存命中
        }

        // 2. 缓存未命中,查数据库
        OrderDetailVO result = queryFromDB(orderId);

        // 3. 写入缓存(设置过期时间)
        redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);

        return result;
    }

    private OrderDetailVO queryFromDB(Long orderId) {
        // 原有数据库查询逻辑
    }
}

3.2 缓存一致性

订单状态变更时,删除缓存(不用更新缓存,避免并发问题):

@Service
public class OrderUpdateService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private OrderMapper orderMapper;

    public void updateOrderStatus(Long orderId, String status) {
        // 1. 先更新数据库
        orderMapper.updateStatus(orderId, status);

        // 2. 再删除缓存
        redisTemplate.delete("order:detail:" + orderId);
    }
}

3.3 防止缓存穿透

查询不存在的订单,会反复打 DB。用空值缓存:

public OrderDetailVO queryOrder(Long orderId) {
    String cacheKey = ORDER_CACHE_KEY + orderId;
    String nullKey = ORDER_NULL_KEY + orderId;

    // 1. 查空值缓存(防止穿透)
    if (Boolean.TRUE.equals(redisTemplate.hasKey(nullKey))) {
        return null;  // 空值缓存命中,直接返回 null
    }

    // 2. 查正常缓存
    OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return cached;
    }

    // 3. 查数据库
    OrderDetailVO result = queryFromDB(orderId);

    if (result == null) {
        // 4. 空值缓存 5 分钟
        redisTemplate.opsForValue().set(nullKey, "", 5, TimeUnit.MINUTES);
    } else {
        // 5. 正常缓存 30 分钟
        redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
    }

    return result;
}

3.4 防止缓存击穿

热点 Key 突然失效,大量请求打到 DB。用互斥锁:

public OrderDetailVO queryOrderWithLock(Long orderId) {
    String cacheKey = ORDER_CACHE_KEY + orderId;
    String lockKey = "lock:order:" + orderId;

    // 1. 先查缓存
    OrderDetailVO cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return cached;
    }

    // 2. 获取分布式锁
    Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(locked)) {
        try {
            // 3. 双重检查
            cached = (OrderDetailVO) redisTemplate.opsForValue().get(cacheKey);
            if (cached != null) {
                return cached;
            }

            // 4. 查数据库
            OrderDetailVO result = queryFromDB(orderId);
            redisTemplate.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
            return result;
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        // 5. 没拿到锁,稍等重试
        Thread.sleep(50);
        return queryOrderWithLock(orderId);
    }
}

3.5 性能对比

优化前:800ms
优化后:50ms(缓存命中时)
提升:94%
场景耗时
缓存命中3-5ms
缓存未命中(查 DB + 写缓存)800ms
缓存击穿(有锁保护)800ms(仅 1 个请求)

Step 4:并行查询(50ms → 30ms)

4.1 串行查询的问题

缓存未命中时,仍然要查 DB。当前是串行的:

查订单(15ms) → 查用户(10ms) → 查商品(20ms) → 查物流(15ms)
总耗时:60ms(串行累加)

这些查询之间没有依赖关系(订单查出来后,用户/商品/物流可以并行),可以改为并行。

4.2 用 CompletableFuture 并行

@Service
public class OrderQueryService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private LogisticsMapper logisticsMapper;

    // 自定义线程池(不要用 ForkJoinPool.commonPool)
    private final ExecutorService executor = Executors.newFixedThreadPool(
            20, new ThreadFactoryBuilder().setNameFormat("order-query-%d").build());

    public OrderDetailVO queryOrderParallel(Long orderId) {
        // 1. 先查订单(后续查询依赖订单的 userId)
        Order order = orderMapper.selectById(orderId);
        if (order == null) {
            return null;
        }

        // 2. 并行查询用户、商品、物流
        CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
                () -> userMapper.selectById(order.getUserId()), executor);

        CompletableFuture<List<OrderItem>> itemsFuture = CompletableFuture.supplyAsync(
                () -> orderItemMapper.selectByOrderId(orderId), executor);

        CompletableFuture<Logistics> logisticsFuture = CompletableFuture.supplyAsync(
                () -> logisticsMapper.selectByOrderId(orderId), executor);

        // 3. 等待全部完成
        CompletableFuture.allOf(userFuture, itemsFuture, logisticsFuture).join();

        // 4. 组装结果
        return buildVO(order, userFuture.join(), itemsFuture.join(), logisticsFuture.join());
    }
}

4.3 并行前后对比

串行:15ms + 10ms + 20ms + 15ms = 60ms
并行:max(10ms, 20ms, 15ms) + 15ms = 35ms

4.4 线程池配置要点

// ❌ 不要用默认线程池
CompletableFuture.supplyAsync(() -> query());
// 默认用 ForkJoinPool.commonPool,线程数 = CPU 核心数 - 1
// 高并发下会被打满

// ✅ 自定义线程池
private final ExecutorService executor = new ThreadPoolExecutor(
    10,                              // 核心线程数
    50,                              // 最大线程数
    60L, TimeUnit.SECONDS,           // 空闲超时
    new LinkedBlockingQueue<>(1000), // 队列
    new ThreadFactoryBuilder().setNameFormat("order-query-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用者执行
);

4.5 性能对比

优化前:50ms(缓存未命中时 800ms)
优化后:30ms(缓存未命中时 500ms)
提升:40%

注意:并行查询的收益在缓存未命中时更明显(800ms → 500ms)。缓存命中时收益小(5ms → 3ms),因为 Redis 本身很快。


Step 5:字段裁剪(30ms → 25ms)

5.1 SELECT * 的问题

// ❌ 原代码
Order order = orderMapper.selectById(orderId);
// 对应 SQL:SELECT * FROM orders WHERE order_id = 12345;

SELECT * 会返回所有字段,包括:

  • description TEXT 类型,平均 5KB
  • remark TEXT 类型,平均 2KB
  • extra_json JSON 类型,平均 3KB

一个订单返回 10KB+ 数据,但接口只需要 20 个字段中的 8 个。

5.2 只查需要的字段

// ✅ 优化后:只查需要的字段
@Select("SELECT order_id, user_id, order_no, status, amount, create_time, pay_time " +
        "FROM orders WHERE order_id = #{orderId}")
Order selectByIdWithFields(Long orderId);

或者用 MyBatis 的 ResultMap:

// Mapper 接口
@Select("SELECT order_id, user_id, order_no, status, amount, create_time " +
        "FROM orders WHERE order_id = #{orderId}")
@Results({
    @Result(property = "orderId", column = "order_id"),
    @Result(property = "userId", column = "user_id"),
    @Result(property = "orderNo", column = "order_no"),
    @Result(property = "status", column = "status"),
    @Result(property = "amount", column = "amount"),
    @Result(property = "createTime", column = "create_time")
})
Order selectSummary(Long orderId);

5.3 网络传输对比

SELECT *                    → 返回 10KB/行
SELECT 8 个字段              → 返回 0.5KB/行
减少:95% 网络传输

5.4 覆盖索引优化

如果查询的字段都在索引里,还能走覆盖索引,避免回表:

-- 创建联合索引(覆盖查询字段)
ALTER TABLE orders ADD INDEX idx_order_covering (
    order_id, user_id, order_no, status, amount
);

-- 查询只查索引字段 → 走覆盖索引,不回表
SELECT order_id, user_id, order_no, status, amount
FROM orders WHERE order_id = 12345;

5.5 性能对比

优化前:30ms
优化后:25ms
提升:17%

这一步提升不大,但在高 QPS 下,网络带宽节省明显。10000 QPS × 10KB = 100MB/s,优化后 5MB/s。


Step 6:批量合并(25ms → 20ms)

6.1 N+1 问题

原始代码中查商品列表是 N+1:

// ❌ N+1 问题
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);  // 1 次查询
List<Product> products = new ArrayList<>();
for (OrderItem item : items) {
    // N 次查询!如果订单有 10 个商品,就查 10 次数据库
    Product product = productMapper.selectById(item.getProductId());
    products.add(product);
}

10 个商品 = 1 + 10 = 11 次 DB 查询。

6.2 批量查询

// ✅ 批量查询:1 + 1 = 2 次 DB 查询
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);

// 收集所有 productId
List<Long> productIds = items.stream()
        .map(OrderItem::getProductId)
        .distinct()
        .collect(Collectors.toList());

// 一次性查所有商品
List<Product> products = productMapper.selectBatchIds(productIds);

// 转成 Map 方便组装
Map<Long, Product> productMap = products.stream()
        .collect(Collectors.toMap(Product::getProductId, p -> p));

// 组装
for (OrderItem item : items) {
    item.setProduct(productMap.get(item.getProductId()));
}

6.3 批量查询 SQL

-- 批量查询(IN 语句)
SELECT * FROM products WHERE product_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

6.4 性能对比

优化前(N+1):1 + 10 = 11 次 DB 查询,耗时 25ms
优化后(批量):1 + 1 = 2 次 DB 查询,耗时 20ms
减少:9 次 DB 往返

6.5 列表场景的 N+1 更严重

单订单查询 N+1 影响 5ms,但订单列表查询影响更大:

// ❌ 订单列表的 N+1(灾难级)
List<Order> orders = orderMapper.selectPage();  // 20 个订单
for (Order order : orders) {
    User user = userMapper.selectById(order.getUserId());       // 20 次
    List<OrderItem> items = orderItemMapper.selectByOrderId();   // 20 次
    Logistics logistics = logisticsMapper.selectByOrderId();     // 20 次
}
// 总计:1 + 20×3 = 61 次 DB 查询!

// ✅ 优化后:批量查询
List<Order> orders = orderMapper.selectPage();

// 批量查用户
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Map<Long, User> userMap = userMapper.selectBatchIds(userIds).stream()
        .collect(Collectors.toMap(User::getUserId, u -> u));

// 批量查订单项
List<Long> orderIds = orders.stream().map(Order::getOrderId).collect(Collectors.toList());
Map<Long, List<OrderItem>> itemMap = orderItemMapper.selectByOrderIds(orderIds).stream()
        .collect(Collectors.groupingBy(OrderItem::getOrderId));

// 批量查物流
Map<Long, Logistics> logisticsMap = logisticsMapper.selectByOrderIds(orderIds).stream()
        .collect(Collectors.toMap(Logistics::getOrderId, l -> l));

// 组装
for (Order order : orders) {
    order.setUser(userMap.get(order.getUserId()));
    order.setItems(itemMap.get(order.getOrderId()));
    order.setLogistics(logisticsMap.get(order.getOrderId()));
}
// 总计:4 次 DB 查询

Step 7:异步化非核心数据(20ms → 200ms*)

7.1 思路转变

前面 6 步都是"怎么查得更快",第 7 步换思路:不是所有数据都需要同步返回

订单详情页的数据分两类:

数据类型例子是否必须同步
核心数据订单状态、金额、商品列表是,用户第一眼要看
非核心数据物流轨迹、推荐商品、优惠券列表否,可以异步加载

7.2 异步加载方案

接口只返回核心数据,非核心数据通过另一个接口异步加载:

// 主接口:只返回核心数据
@GetMapping("/api/orders/{orderId}")
public OrderDetailVO queryOrder(@PathVariable Long orderId) {
    // 只查核心数据:订单 + 用户 + 商品
    OrderCoreVO core = orderQueryService.queryOrderCore(orderId);
    return core;
    // 耗时:200ms
}

// 异步接口:前端拿到核心数据后,再调这个接口拿非核心数据
@GetMapping("/api/orders/{orderId}/extra")
public OrderExtraVO queryOrderExtra(@PathVariable Long orderId) {
    // 查非核心数据:物流轨迹 + 推荐 + 优惠券
    return orderQueryService.queryOrderExtra(orderId);
}

前端配合:

// 1. 先请求核心数据(200ms 返回)
const order = await fetch(`/api/orders/${orderId}`);
renderOrderBasic(order);  // 立即渲染订单基本信息

// 2. 再请求非核心数据(异步,不阻塞首屏)
fetch(`/api/orders/${orderId}/extra`).then(extra => {
    renderLogistics(extra.logistics);
    renderRecommendations(extra.recommendations);
});

7.3 更彻底的异步:CompletableFuture + 数据库消息

对于"不必须实时"的数据,写入时异步更新:

// 更新订单时,异步更新物流信息
public void updateOrder(Long orderId) {
    // 1. 同步:更新订单核心数据
    orderMapper.update(order);

    // 2. 异步:更新物流、推荐等
    CompletableFuture.runAsync(() -> {
        logisticsService.refreshLogistics(orderId);
    }, asyncExecutor);

    CompletableFuture.runAsync(() -> {
        recommendService.refreshRecommendation(orderId);
    }, asyncExecutor);
}

7.4 性能对比

优化前(全量同步返回):20ms(但有 5ms 是非核心数据)
优化后(核心数据同步):200ms*(接口整体从 5s 降到 200ms)

* 注:200ms 是包含了网络传输、序列化等完整链路的时间
  纯 DB 查询部分从 5s 降到 20ms,接口整体 200ms

7.5 用户体验对比

优化前:
  用户点击 → 等 5 秒 → 看到完整页面

优化后:
  用户点击 → 等 200ms → 看到核心信息(订单状态、金额、商品)
         → 再等 500ms → 看到物流、推荐(用户已经在看核心信息了)

用户感知:从"卡 5 秒"变成"秒开"。


总结

7 步优化总览

步骤优化手段提升层次
1加索引5120ms1200ms76%数据库
2减少联表1200ms800ms33%SQL
3Redis 缓存800ms50ms94%缓存
4并行查询50ms30ms40%多线程
5字段裁剪30ms25ms17%SQL
6批量合并25ms20ms20%SQL
7异步化20ms200ms*-架构

优化优先级

收益大 → 小:
  1. 加索引(76%)         ← 必做,性价比最高
  2. 加缓存(94%)         ← 必做,立竿见影
  3. 减少 N+1              ← 必做,影响大
  4. 减少联表              ← 视情况
  5. 异步化                ← 视业务场景
  6. 并行查询              ← 收益有限
  7. 字段裁剪              ← 高 QPS 下有用

优化原则

  1. 先定位,再优化:用 EXPLAIN、慢查询日志、APM 工具找到瓶颈
  2. 先数据库,再缓存:数据库层的问题(没索引、N+1)不要用缓存掩盖
  3. 先监控,再优化:没有数据支撑的优化是盲目的
  4. 先简单,再复杂:加索引 5 分钟,改架构 5 天
  5. 不是所有优化都值得做:20ms 到 15ms 的优化,不如把时间花在别的地方

排查工具清单

工具用途
MySQL 慢查询日志找慢 SQL
EXPLAIN分析执行计划
p6spy打印 SQL 执行时间
Arthas线上方法耗时诊断
SkyWalking / Jaeger全链路追踪
JMeter / wrk压测
Redis slowlogRedis 慢查询

互动话题:你遇到过最慢的接口有多慢?最后怎么优化的?欢迎留言分享!


参考资料


标题:接口性能优化:从 5 秒到 200ms 的 7 步实战调优全记录
作者:jiangyi
地址:http://jiangyi.space/articles/2026/08/04/1785577251074.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消