我是怎么把一个“越改越慢”的接口,优化到稳定 100ms 的
在做业务开发时,我们很容易遇到一种情况:
一个接口最开始很快,但随着需求不断叠加,慢慢变得越来越慢,直到某一天被用户投诉。
最近我就接手了这样一个接口,从“能用”到“可用”,再到“可维护”,中间做了一次完整的优化,这里分享一下整个过程。
一、问题背景
这是一个典型的列表查询接口,功能是:
- 查询用户的订单列表
- 支持分页
- 支持状态筛选
- 返回订单 + 部分关联信息
初始情况:
- 数据量:约 50 万
- 平均耗时:800ms ~ 1.5s
- 高峰期偶尔超过 3 秒
对于一个列表接口来说,这个性能显然不太能接受。
二、第一步:不要急着优化,先“拆解问题”
很多人一上来就开始:
- 加缓存
- 改SQL
- 上索引
但如果没有搞清楚慢在哪里,很容易“优化了个寂寞”。
我先做了一件很基础但很关键的事情:
👉 把接口耗时拆开
大致拆成三块:
- 参数处理
- 数据库查询
- 数据组装(包括关联查询)
加日志统计后,结果很清晰:
- 数据库查询:约 70%
- 数据组装:约 25%
- 其他:5%
👉 结论:主要瓶颈在数据库,其次是数据处理逻辑
三、第二步:SQL真的“简单”吗?
原始SQL大致是这样(简化版):
SELECT * FROM orders
WHERE user_id = ?
AND status = ?
ORDER BY create_time DESC
LIMIT ?, ?;
看起来没问题,但有几个隐患:
1. 使用了 SELECT *
这在数据字段较多时,会带来:
- 不必要的IO开销
- 网络传输浪费
👉 优化:只查必要字段
2. 分页越往后越慢
当分页 offset 很大时,比如:
LIMIT 10000, 10;
数据库需要:
- 先扫描 10010 条
- 再丢弃前 10000 条
👉 典型“深分页问题”
四、第三步:优化思路(分层处理)
这次优化我没有只做一件事,而是分三层处理。
✅ 优化一:建立正确的索引
原来只有单列索引:
(user_id)
但实际查询条件是:
- user_id
- status
- create_time(排序)
👉 最终调整为联合索引:
(user_id, status, create_time)
效果:
- 查询时间明显下降
- 执行计划稳定
✅ 优化二:改造分页方式(关键优化)
把传统分页:
LIMIT offset, size
改为:
👉 基于游标(cursor)的分页
思路:
WHERE create_time < last_time
ORDER BY create_time DESC
LIMIT 10;
优点:
- 不依赖 offset
- 性能稳定
- 越往后越快
这个改动对性能提升非常明显。
✅ 优化三:减少“隐形N+1查询”
原来的数据组装逻辑是:
- 查订单列表
- 循环每条订单,再查用户信息 / 商品信息
👉 典型N+1问题
优化方式:
- 一次性批量查询关联数据
- 使用 map 做内存关联
改完之后:
- 数据处理时间直接下降一半
五、优化结果
最终效果:
- 平均耗时:从 ~1s 降到 ~100ms
- 接口稳定性明显提升
- 高峰期无明显波动
六、这次优化最大的收获(不是技术点)
比起具体优化手段,我觉得更重要的是这几点:
1. 不要用“感觉”优化
很多时候我们会觉得:
- “肯定是SQL慢”
- “加个缓存就好了”
但实际上:
👉 数据才是唯一依据
2. 性能问题往往是“组合问题”
这次慢,不是一个点导致的,而是:
- 索引不合理
- 分页方式不对
- 数据处理低效
👉 多个小问题叠加 = 大问题
3. 越早规范,成本越低
这些问题在数据量小的时候:
- 完全看不出来
但一旦上量:
👉 全部暴露
七、给自己的一个复盘清单
现在我在写类似接口时,会默认检查:
- 是否使用了联合索引?
- 是否存在深分页问题?
- 是否避免了 SELECT *?
- 是否有N+1查询?
- 是否可以减少不必要的数据?
这套检查下来,基本能规避 80% 的性能坑。
结语
很多性能问题,其实并不复杂,难的是:
在“看起来能用”的状态下,是否愿意去优化它
这次优化让我更清楚一件事:
👉 好的代码,不只是能跑,还要“跑得稳、跑得久”。