背景
订单列表接口在数据量上来后 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 里带上版本号,避免出现"删了缓存但旧值又被写回"的竞态。
优化效果
经验总结
- 优化顺序:先定位(慢日志)→ 再减量(N+1、无用字段)→ 最后加缓存。反过来做等于掩盖问题
- 不要 SELECT *,只取序列化需要的字段,IO 和网络开销都会显著下降
- 加缓存前先想清楚失效策略,缓存不一致造成的线上事故比慢接口更难排查