接口响应从 800ms 到 80ms 的优化之路

背景 订单列表接口在数据量上来后 P99 响应达到 800ms,用户明显感知卡顿。目标:压到 100ms 以内,不升级硬件。 第一步:慢查询定位(800ms → 300ms) 开启慢查询日志,发现列表页的统计子查询没走索引: EXPLAIN SELECT o.*, (SELECT COUNT(*) FROM order_item i WHERE i.order_id = o.id) AS item_count FROM orders o WHERE o.user_id = ? ORDER BY created_at DESC LIMIT 20; order_item.order_id 没有索引,子查询对每行订单做全表扫描。加上索引后接口降到 300ms。 第一刀永远先看慢查询日志。大部分"接口慢"的本质是"SQL 慢"。 第二步:消灭 N+1 查询(300ms → 120ms) ORM 的懒加载在序列化时触发了 N+1:列表 20 条订单,每条又单独查一次用户信息和商品快照,一次请求实际执行了 40+ 条 SQL。改成批量查询 + 内存组装: orders = orderRepo.findByUser(userId, 20); userMap = userRepo.findByIds(orders.map(o => o.userId)); itemMap = itemRepo.findByOrderIds(orders.map(o => o.id)); // 内存里组装,总共 3 条 SQL 第三步:缓存热点数据(120ms → 80ms) 用户信息、商品快照这类读多写少的数据加 Redis 缓存,TTL 5 分钟 + 更新时主动失效。注意缓存 key 里带上版本号,避免出现"删了缓存但旧值又被写回"的竞态。 优化效果 优 基 + + + 化 线 项 索 批 R 引 量 e 修 查 d 复 询 i 消 s 灭 热 N 点 + 缓 1 存 3 1 耗 0 2 8 时 8 0 0 0 变 0 m m m 化 0 s s s m s 经验总结 优化顺序:先定位(慢日志)→ 再减量(N+1、无用字段)→ 最后加缓存。反过来做等于掩盖问题 不要 SELECT *,只取序列化需要的字段,IO 和网络开销都会显著下降 加缓存前先想清楚失效策略,缓存不一致造成的线上事故比慢接口更难排查

August 15, 2026 · 1 min · 152 words · 技术小站