1. 接口超时优化:从 5 秒到 500ms 的踩坑之路
我先问你个问题:线上一个接口 5 秒才返回,你慌不慌?
说实话,我第一反应是不慌——先看日志,再看慢查询。但那次,我整整熬了两天。
一个看起来很简单的订单查询接口,日均调用 200 万次,耗时稳定在 5 秒左右。用户疯狂投诉,老板拍桌子。我查了数据库索引、缓存、网络带宽,全都没问题。
那问题出在哪儿?
嗯,这就是今天我们聊的第一个故事。
1.1 场景还原:一个“简单”的订单列表
接口功能:分页查询用户近 30 天的订单,每次返回 20 条。后台用 MySQL + Redis。逻辑大致这样:
- 先查 Redis 缓存,如果命中就直接返回。
- 没命中则查 MySQL,写回 Redis。
你可能会说:“就这?能慢到 5 秒?”
我当时也这么想的。直到我把 SQL 捞出来一看——好家伙,单条 SQL 包含了 6 张表的 LEFT JOIN,还有两个子查询。其中一张订单快照表有 800 万行。
1.2 第一轮踩坑:加索引能解决一切?
我的第一反应:加索引。快刀斩乱麻。
我挑了订单表的时间字段和用户 ID,加了个联合索引。效果如何?
从 5 秒降到 4.2 秒。嗯,微乎其微。
为什么?因为主查询最耗时的是那张 800 万行的快照表,而快照表的关联字段(订单 ID)本身已经是主键索引。再多加索引也帮不上忙。
| 优化操作 | 优化前耗时 | 优化后耗时 | 变化 |
|---|---|---|---|
| 增加联合索引(用户+时间) | 5 000 ms | 4 200 ms | 🔻 16% |
| 移除不必要的 LEFT JOIN | 4 200 ms | 3 100 ms | 🔻 26% |
| 分页优化(延迟关联) | 3 100 ms | 1 500 ms | 🔻 52% |
| 引入本地缓存 + 异步预热 | 1 500 ms | 480 ms | 🔻 68% |
每一轮优化都有用,但只有组合拳才能拉到 500ms 以内。
1.3 第二刀:SQL 重构与延迟关联
我决定重构 SQL。核心思路:把大 JOIN 拆成两步。
- 先只查主表的主键 ID,走覆盖索引,非常快。
- 再用主键 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 秒。
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 知识体系与复盘框架
一个接口优化,不能光靠瞎猜。我总结了一套四步框架,每次出问题我就这么推。
这个框架,是我在好几个项目里打磨出来的。每次遇到超时,先问自己:指标是否准确?瓶颈是否唯一?方案有没有验证环境?不小心漏掉哪一步,下次还会摔。
1.6 总结:从 5 秒到 500ms 的关键动作
- 把大 JOIN 拆成“主键查询 + 回表” —— 这是最快见效的刀法。
- 引入本地缓存 + 互斥锁,不要全依赖 Redis。
- 分页使用延迟关联,避免 MySQL 翻页丢行。
- 别忘了压测验证,用真实流量(甚至放大 2 倍)来测试。
接口优化不是一锤子买卖。你优化完一轮,过两个月流量翻倍,可能又得重来。但只要保持复盘的习惯,形成自己的排查清单——我保证,你会越来越快。