09 - 一个高并发接口的优化实录
前八章讲了一堆手段。这一章把它们串起来,用一个虚构但典型的场景——「电商首页商品列表接口」——演示优化是怎么一步步推进的。
重点不是数字(数字是示意),而是推导的思路和顺序。
一句话先记住:优化是「测量 → 找瓶颈 → 针对性下手 → 再测量」的循环,每次只解决当前最大的瓶颈。
9.0 场景设定
接口:GET /api/products (首页商品列表,读多写少)
初始情况:
· 单台 FastAPI,同步写法
· 每次请求都查 PostgreSQL,一句复杂 SQL(多表 JOIN)
· 压测结果:QPS ≈ 200,响应时间 ≈ 500ms
目标:扛住大促流量,QPS 2 万+
第 1 轮:优化慢查询(先修最痛的)
压测发现:500ms 里有 450ms 花在那句 JOIN 的 SQL 上。瓶颈在数据库查询本身。
动作:
- 用
EXPLAIN ANALYZE分析,发现漏了索引 → 加索引 - 简化 JOIN、去掉不必要的字段
结果:SQL 从 450ms → 50ms,QPS ≈ 200 → 1500。
教训:先优化「单次请求本身有多慢」,别的手段都是后话。(索引优化见系列三)
第 2 轮:加缓存(拦住重复的读)
分析:商品列表内容变化不频繁,但每个请求都查库,纯属重复劳动。
动作:
- 上 Redis 缓存,商品列表缓存 60 秒(旁路缓存模式)
- 处理缓存穿透/击穿/雪崩(缓存空值、加随机过期、热点互斥锁)
结果:绝大多数请求直接命中缓存(内存读 ≈ 2ms),只有极少数回源查库。QPS ≈ 1500 → 8000。
优化前:每个请求都查库
优化后:
10000 请求 → 缓存命中 9500(2ms)
→ 回源查库 500(50ms)
教训:读多写少 + 能容忍短暂不一致,缓存是性价比之王。
第 3 轮:改异步 + 调连接池(榨干单机)
分析:同步写法下,请求等 IO 时 CPU 在发呆;连接池太小,高并发时请求排队等连接。
动作:
- 接口改成
async def,用异步数据库驱动 - 连接池大小压测调优到「刚好够用」
- 部署改成「多进程(用满核)+ 每进程异步」
结果:单机吞吐大幅提升,等待时间被利用起来。QPS ≈ 8000 → 15000。
教训:并发模型选对,单机就能翻几倍(第 08 章)。
第 4 轮:水平扩展 + 负载均衡(加机器)
分析:单机已榨干,要再上就得加机器。
动作:
- 应用无状态化(Session/缓存都放 Redis)
- 跑多台 FastAPI 实例,前面 Nginx 负载均衡(轮询)
- 数据库读压力大 → 加从库,读写分离
结果:QPS 随机器数量近似线性增长。3 台 → QPS ≈ 4 万+。
教训:无状态是水平扩展的前提(第 06 章)。
第 5 轮:加保护(防止被打崩)
分析:大促可能来超预期流量,得给系统兜底。
动作:
- 网关 + 应用限流(令牌桶,超出容量的请求直接拒绝)
- 大促时降级:关掉「猜你喜欢」等非核心模块
- 对下游依赖加熔断,防止某个服务挂了拖垮全局
结果:即使流量超预期,系统也「部分可用」而非全崩。
教训:扛不住时,丢卒保车(第 07 章)。
9.1 复盘:优化的推进顺序
① 优化单次请求本身 (索引、SQL) ← 最先做,成本最低
② 加缓存 (拦住重复读) ← 性价比之王
③ 榨干单机 (异步、连接池) ← 免费提升吞吐
④ 水平扩展 (加机器、读写分离) ← 花钱换上限
⑤ 加保护 (限流降级熔断) ← 兜底,防崩溃
注意这个顺序背后的逻辑:从「便宜且见效快」到「昂贵但上限高」,从「提升能力」到「兜底保护」。
9.2 关键心态
- 不要跳步:没加索引就想着分库分表,是本末倒置
- 每次只解决当前最大瓶颈:解决完再压测,瓶颈会转移到下一处
- 数字要靠压测得出:本章的 QPS 全是示意,真实项目一切以压测为准
本章小结
| 轮次 | 手段 | 解决什么 |
|---|---|---|
| 1 | 索引 / SQL 优化 | 单次请求太慢 |
| 2 | 缓存 | 重复的读打到库 |
| 3 | 异步 + 连接池 | 单机没榨干 |
| 4 | 水平扩展 + 读写分离 | 单机到顶了 |
| 5 | 限流降级熔断 | 防止被打崩 |
下一章预告:全程都在说「压测发现……」,那压测到底怎么做?关键要监控哪些指标?
上一章 ← 08 - 并发模型与异步 IO | 下一章 → 10 - 压测与监控