后端调优实战:从响应慢到秒开的几个关键动作

上周帮朋友看一个电商后台接口,明明只查一张订单表,却要等3秒才返回。他一脸无奈:‘加了缓存、开了连接池,咋还这么卡?’

后端调优不是玄学,也不是堆服务器就能解决的事。它更像修车——得听声音、看仪表、摸温度,再一个个拧紧松动的螺丝。

先抓“真凶”,别猜

别急着改代码。先用 curl -w '@curl-format.txt' -o /dev/null -s http://localhost:8080/api/order/123' 看下各阶段耗时(DNS、连接、TTFB、传输)。很多“慢”其实卡在数据库连接建立或 DNS 解析上,跟业务逻辑半毛钱关系没有。

数据库:别让 SELECT * 当主角

翻了下他的 SQL 日志,发现一个接口执行了 7 次 SELECT *,每次查的都是同一张用户表,只为了取 nicknameavatar_url。改成 SELECT nickname, avatar_url FROM users WHERE id = ?,QPS 立刻从 42 涨到 186。

顺手加个复合索引:

ALTER TABLE users ADD INDEX idx_id_nickname_avatar (id, nickname, avatar_url);

HTTP 层:能不重传就不重传

前端反复拉同一个商品详情页,后端却每次都重新渲染 JSON。加上 ETag 或 Last-Modified 响应头,配合 If-None-Match 请求头,命中缓存直接返回 304,省掉序列化+DB 查询两步。

Spring Boot 示例:

@GetMapping("/product/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id, HttpServletRequest req) {
    Product p = productService.findById(id);
    String etag = "\"" + p.getId() + "-" + p.getUpdatedAt().hashCode() + "\"";
    if (req.getHeader("If-None-Match") != null &&
        req.getHeader("If-None-Match").equals(etag)) {
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
    }
    return ResponseEntity.ok().eTag(etag).body(p);
}

线程池:别让 Tomcat 自己瞎扛

默认 Tomcat 的 maxThreads=200,看着不少,但遇到 IO 密集型任务(比如调第三方支付接口),线程全卡在等待响应上,新请求只能排队。把耗时操作扔进独立线程池:

@Async("paymentTaskExecutor")
public void sendPaymentResult(String orderId) { ... }
并配置:
spring.task.execution.pool.max-size=50
spring.task.execution.pool.queue-capacity=200

最后一步:压测别只跑 happy path

用 wrk 模拟真实场景:

wrk -t4 -c200 -d30s --latency 'http://localhost:8080/api/order?uid=1001'
观察平均延迟、99 分位延迟和错误率。如果 99 分位飙到 2.3 秒,而平均才 320ms,说明有少量请求被拖垮——大概率是某次慢查询或锁表没发现。

调优不是一锤子买卖。上线后盯三天 Grafana 的 GC 时间、DB 连接数、HTTP 5xx 曲线,比任何文档都管用。