Skip to content

接口性能优化 Checklist

想象一下这个场景:你的接口响应时间是 500ms,用户抱怨体验差。你开始优化——加缓存、改数据库索引、优化 SQL。折腾了一周,响应时间降到了 450ms。

但等等,你确定找对方向了吗?

性能优化的第一步,不是动手,而是系统性地诊断。很多时候,真正拖慢接口的不是你以为的那个环节。这份 Checklist,就是帮你快速定位瓶颈的「体检单」。


一、网络层检查

1.1 DNS 解析耗时

每次发起 HTTP 请求前,浏览器或客户端需要解析域名。如果 DNS 解析耗时过长,整个请求链条都会被拖慢。

java
// 使用DnsResolver缓存DNS结果,避免重复解析
import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;

public class DnsCache {
    private static final ConcurrentHashMap<String, InetAddress> cache = new ConcurrentHashMap<>();
    private static final long TTL = 60_000; // 缓存60秒

    public static InetAddress resolve(String host) throws Exception {
        long now = System.currentTimeMillis();
        Entry entry = cache.get(host);

        if (entry != null && (now - entry.timestamp) < TTL) {
            return entry.address;
        }

        InetAddress address = InetAddress.getByName(host);
        cache.put(host, new Entry(address, now));
        return address;
    }

    private static class Entry {
        final InetAddress address;
        final long timestamp;

        Entry(InetAddress address, long timestamp) {
            this.address = address;
            this.timestamp = timestamp;
        }
    }
}

检查项:

  • 是否使用了 CDN 预解析?
  • DNS 缓存策略是否合理?
  • 是否使用了 HTTP/2 或 HTTP/3 减少连接建立次数?

1.2 TCP 连接建立耗时

三次握手需要 1.5-2 个 RTT(Round Trip Time)。如果接口依赖多个下游服务,光建立连接就能耗掉几十毫秒。

java
// 配置连接池,复用HTTP连接
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

public class HttpClientFactory {
    public static CloseableHttpClient createOptimizedClient() {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(200);          // 最大连接数
        cm.setDefaultMaxPerRoute(50); // 每个路由最大连接数

        return HttpClients.custom()
                .setConnectionManager(cm)
                .setKeepAliveStrategy((response, context) -> 30_000) // Keep-Alive 30秒
                .build();
    }
}

检查项:

  • 是否启用了 HTTP Keep-Alive?
  • 连接池配置是否合理?
  • 是否存在大量短连接?

二、数据传输层检查

2.1 响应体大小

一次接口调用返回 5MB 的 JSON 数据,网络传输就要几十秒。优化响应体大小,往往是最立竿见影的手段。

java
// 使用GZIP压缩响应
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;

public class CompressionUtil {

    public static byte[] compressGzip(String data) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
            gzip.write(data.getBytes("UTF-8"));
        }
        return bos.toByteArray();
    }

    // 估算压缩效果
    public static void main(String[] args) throws IOException {
        String json = generateLargeJson(); // 假设这是待压缩的数据

        byte[] compressed = compressGzip(json);
        double ratio = (1 - (double) compressed.length / json.length()) * 100;

        System.out.printf("原始大小: %d bytes%n", json.length());
        System.out.printf("压缩后: %d bytes%n", compressed.length);
        System.out.printf("压缩率: %.1f%%%n", ratio);
    }
}

检查项:

  • 是否开启了响应压缩(GZIP/Br/Zstd)?
  • 是否存在 N+1 查询导致返回过多数据?
  • 是否可以分页或懒加载?

2.2 序列化效率

JSON 序列化/反序列化是 CPU 密集型操作。选择高效的序列化方案,能显著降低延迟。

java
// 对比JSON和Protobuf的序列化性能
public class SerializationBenchmark {

    public static void main(String[] args) throws Exception {
        User user = new User(1L, "张三", "zhangsan@example.com", 28);

        // JSON序列化
        long jsonStart = System.nanoTime();
        ObjectMapper mapper = new ObjectMapper();
        byte[] jsonBytes = mapper.writeValueAsBytes(user);
        long jsonTime = System.nanoTime() - jsonStart;

        // Protobuf序列化
        UserProto.User protoUser = UserProto.User.newBuilder()
                .setId(user.getId())
                .setName(user.getName())
                .setEmail(user.getEmail())
                .setAge(user.getAge())
                .build();
        long protoStart = System.nanoTime();
        byte[] protoBytes = protoUser.toByteArray();
        long protoTime = System.nanoTime() - protoStart;

        System.out.printf("JSON: %d bytes, %d μs%n", jsonBytes.length, jsonTime / 1000);
        System.out.printf("Protobuf: %d bytes, %d μs%n", protoBytes.length, protoTime / 1000);
    }
}

检查项:

  • 是否使用了高效的序列化方案?
  • 内部通信是否可以用 Protobuf/Kryo?
  • 是否有不必要的深拷贝?

三、业务逻辑层检查

3.1 数据库查询

数据库往往是接口延迟的最大来源。90% 的性能问题,都出在 SQL 上。

java
// 避免N+1查询,使用批量查询
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper itemMapper;

    // 低效写法:N+1查询
    public List<OrderVO> getOrdersBad(Long userId) {
        List<Order> orders = orderMapper.findByUserId(userId);
        return orders.stream().map(order -> {
            List<OrderItem> items = itemMapper.findByOrderId(order.getId()); // N次查询
            return toVO(order, items);
        }).collect(Collectors.toList());
    }

    // 优化写法:批量查询
    public List<OrderVO> getOrdersGood(Long userId) {
        List<Order> orders = orderMapper.findByUserId(userId);
        if (orders.isEmpty()) {
            return Collections.emptyList();
        }

        // 一次查询获取所有订单项
        List<Long> orderIds = orders.stream().map(Order::getId).collect(Collectors.toList());
        Map<Long, List<OrderItem>> itemMap = itemMapper.findByOrderIds(orderIds)
                .stream()
                .collect(Collectors.groupingBy(OrderItem::getOrderId));

        return orders.stream()
                .map(order -> toVO(order, itemMap.getOrDefault(order.getId(), Collections.emptyList())))
                .collect(Collectors.toList());
    }
}

检查项:

  • 是否有 N+1 查询问题?
  • 索引是否命中?
  • 是否可以用缓存减少数据库访问?
  • SQL 是否存在不必要的全表扫描?

3.2 同步调用 vs 异步调用

一个接口依赖 5 个下游服务,每个服务响应时间 50ms。同步调用需要 250ms,异步调用只需要 50ms(取最大值)。

java
// 使用CompletableFuture实现并行调用
@Service
public class UserDetailService {

    public UserDetail getUserDetail(Long userId) {
        // 串行调用:150ms+
        // User user = getUser(userId);
        // List<Order> orders = getOrders(userId);
        // List<Address> addresses = getAddresses(userId);

        // 并行调用:50ms+
        CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser(userId));
        CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> getOrders(userId));
        CompletableFuture<List<Address>> addressesFuture = CompletableFuture.supplyAsync(() -> getAddresses(userId));

        CompletableFuture.allOf(userFuture, ordersFuture, addressesFuture).join();

        return new UserDetail(
                userFuture.get(),
                ordersFuture.get(),
                addressesFuture.get()
        );
    }
}

检查项:

  • 接口中有多少同步下游调用可以改为并行?
  • 是否可以用消息队列解耦非关键路径?
  • 是否有不必要的串行依赖?

四、缓存层检查

4.1 多级缓存策略

java
// 本地缓存 + 分布式缓存二级缓存
@Service
public class ProductCache {

    private final LoadingCache<Long, Product> localCache;
    private final RedisTemplate<String, Product> redisTemplate;

    public ProductCache(RedisTemplate<String, Product> redisTemplate) {
        this.redisTemplate = redisTemplate;

        // Guava本地缓存:热点数据快速访问
        this.localCache = CacheBuilder.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .build(new CacheLoader<Long, Product>() {
                    @Override
                    public Product load(Long id) {
                        return loadFromRedis(id);
                    }
                });
    }

    public Product getProduct(Long id) {
        // L1: 本地缓存
        try {
            return localCache.get(id);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private Product loadFromRedis(Long id) {
        String key = "product:" + id;
        Product product = redisTemplate.opsForValue().get(key);

        if (product == null) {
            product = loadFromDatabase(id);
            redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
        }

        return product;
    }

    private Product loadFromDatabase(Long id) {
        // 从数据库加载
        return null;
    }
}

检查项:

  • 热数据是否上了缓存?
  • 缓存失效策略是否合理?
  • 缓存穿透/击穿/雪崩是否防范?

五、JVM 层检查

5.1 GC 造成的停顿

GC 停顿可能导致接口响应时间抖动。生产环境的 GC 问题,往往在压测时发现不了。

java
// 使用JVM参数优化GC
/*
# G1垃圾收集器配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100          # 最大GC停顿时间
-XX:G1HeapRegionSize=4m          # Region大小
-XX:InitiatingHeapOccupancyPercent=45  # 触发Mixed GC的堆占用比例

# 日志配置
-Xlog:gc*:file=gc.log:time:filecount=5,filesize=10m
*/

检查项:

  • 使用的 GC 收集器是否适合当前场景?
  • Young 区是否足够大?
  • 是否存在内存泄漏导致频繁 Full GC?
  • GC 日志是否定期分析?

六、检查清单速查

层级检查项优先级
网络DNS 解析优化P1
网络HTTP Keep-AliveP1
网络HTTP/2 多路复用P1
传输响应压缩P1
传输高效序列化P1
业务数据库索引P1
业务缓存策略P1
业务异步化改造P2
业务N+1 查询优化P1
JVMGC 优化P2

留给你的问题

一份 Checklist 能帮你找到问题,但解决问题的方案往往需要权衡。

比如:缓存可以提升性能,但会带来数据一致性风险;异步可以提升吞吐,但会增加系统复杂度。

你现在的接口,最主要的瓶颈在哪?为了解决这个问题,你愿意付出多大的代价?

这是一个没有标准答案的问题,但值得你认真思考。

基于 VitePress 构建