1. 接口超时优化:从 5 秒到 500ms 的踩坑之路

我先问你个问题:线上一个接口 5 秒才返回,你慌不慌?

说实话,我第一反应是不慌——先看日志,再看慢查询。但那次,我整整熬了两天。

一个看起来很简单的订单查询接口,日均调用 200 万次,耗时稳定在 5 秒左右。用户疯狂投诉,老板拍桌子。我查了数据库索引、缓存、网络带宽,全都没问题。

那问题出在哪儿?

嗯,这就是今天我们聊的第一个故事。

1.1 场景还原:一个“简单”的订单列表

接口功能:分页查询用户近 30 天的订单,每次返回 20 条。后台用 MySQL + Redis。逻辑大致这样:

  • 先查 Redis 缓存,如果命中就直接返回。
  • 没命中则查 MySQL,写回 Redis。

你可能会说:“就这?能慢到 5 秒?”

我当时也这么想的。直到我把 SQL 捞出来一看——好家伙,单条 SQL 包含了 6 张表的 LEFT JOIN,还有两个子查询。其中一张订单快照表有 800 万行。

⚠️ 早期设计隐患: 团队为了少写代码,把查询字段全塞进一条 SQL 里。单个 JOIN 本身没问题,但多表关联 + 大量字段 + 无合适索引,就是灾难。

1.2 第一轮踩坑:加索引能解决一切?

我的第一反应:加索引。快刀斩乱麻。

我挑了订单表的时间字段和用户 ID,加了个联合索引。效果如何?

从 5 秒降到 4.2 秒。嗯,微乎其微。

为什么?因为主查询最耗时的是那张 800 万行的快照表,而快照表的关联字段(订单 ID)本身已经是主键索引。再多加索引也帮不上忙。

优化操作优化前耗时优化后耗时变化
增加联合索引(用户+时间)5 000 ms4 200 ms🔻 16%
移除不必要的 LEFT JOIN4 200 ms3 100 ms🔻 26%
分页优化(延迟关联)3 100 ms1 500 ms🔻 52%
引入本地缓存 + 异步预热1 500 ms480 ms🔻 68%

每一轮优化都有用,但只有组合拳才能拉到 500ms 以内。

1.3 第二刀:SQL 重构与延迟关联

我决定重构 SQL。核心思路:把大 JOIN 拆成两步。

  1. 先只查主表的主键 ID,走覆盖索引,非常快。
  2. 再用主键 ID 回表查其他字段,利用主键聚簇索引。
-- 原始写法(6 张 JOIN,慢在临时表)  
SELECT o.id, o.user_id, o.amount, s.snapshot_data, …
FROM orders o  
LEFT JOIN order_snapshot s ON o.id = s.order_id  
WHERE o.user_id = ?  
  AND o.create_time >= ?  
ORDER BY o.create_time DESC  
LIMIT 20 OFFSET ?;
-- 优化后:先查主键,再关联
SELECT o.id, o.user_id, o.amount, s.snapshot_data, …
FROM (
  SELECT id FROM orders  
  WHERE user_id = ? AND create_time >= ?  
  ORDER BY create_time DESC  
  LIMIT 20 OFFSET ?
) AS tmp  
JOIN orders o ON tmp.id = o.id  
LEFT JOIN order_snapshot s ON o.id = s.order_id;

这个改动,把临时表操作减少了一大半。执行时间从 3.1 秒降到 1.5 秒。

💡 经验分享: 我个人习惯在分页查询中优先使用“延迟关联”,尤其是当表很大、有多 JOIN 时。不是所有场景都能直接套用,但它常常是 800ms 以下的第一块跳板。

1.4 第三刀:缓存分层,别只指望 Redis

1.5 秒,还有优化空间。但 Redis 缓存已经用上了,为什么还慢?

分析发现:每次用户刷新列表,大量请求同时落到第一页。Redis 缓存确实能顶住第一波,但一旦缓存过期,所有请求一起穿透到 DB——这就是所谓的缓存雪崩 + 击穿。

解决办法:

  • 本地缓存(Caffeine): 热点数据(最近 20 条订单)缓存在 JVM 内,TTL 30 秒。
  • 异步预热: 用户进入界面时,后台用一个线程提前加载下一页数据到 Redis。
  • 互斥锁: 缓存失效时,只有一个线程去 DB 加载,其他线程等待。

改完之后,P99 耗时降到 480ms。

关键指标:
  • 优化前 P99 = 5 000 ms
  • 优化后 P99 = 480 ms
  • DB 查询量下降 85%
  • CPU 使用率从 68% 降至 21%

1.5 知识体系与复盘框架

一个接口优化,不能光靠瞎猜。我总结了一套四步框架,每次出问题我就这么推。

接口优化四步复盘框架 指标检测 耗时/TP/QPS/错误率 定位瓶颈 APM/Slow Log/Profiling 方案选型 SQL/缓存/架构/并发 灰度验证 A/B/压力测试/回滚 灰度验证不通过 → 回退到第一步重新检测 常见误区 1. 只加索引不重构 SQL —— 治标不治本 2. 盲目上缓存却不管穿透击穿 —— 缓存变成装饰品 3. 忽略前端的请求合并 —— 后端优化 10 倍,前端一次发 50 个请求全白费

这个框架,是我在好几个项目里打磨出来的。每次遇到超时,先问自己:指标是否准确?瓶颈是否唯一?方案有没有验证环境?不小心漏掉哪一步,下次还会摔。

💡 避坑指南: 我曾经在定位瓶颈时只看平均耗时,结果 P99 比平均高 10 倍。后来养成习惯:必须看 P99 和 P999,再决定优化重点。

1.6 总结:从 5 秒到 500ms 的关键动作

  • 把大 JOIN 拆成“主键查询 + 回表” —— 这是最快见效的刀法。
  • 引入本地缓存 + 互斥锁,不要全依赖 Redis。
  • 分页使用延迟关联,避免 MySQL 翻页丢行。
  • 别忘了压测验证,用真实流量(甚至放大 2 倍)来测试。
⚠️ 最后一道坎: 优化完成后,我忘了更新 API 文档里标注的响应时间。结果前端对接框架直接超时重试,流量翻倍,又把 DB 打挂了。文档和配置跟代码一样需要维护。

接口优化不是一锤子买卖。你优化完一轮,过两个月流量翻倍,可能又得重来。但只要保持复盘的习惯,形成自己的排查清单——我保证,你会越来越快。