site logo

Marico's space

我是如何将 API 响应时间缩短 94% 的——真正有效的缓存策略

前端技术 2026-07-03 14:49:10 7

半年前,半夜两点我在线上修bug。我们的API响应时间已经飙到3.2秒,用户在群里炸锅,数据库CPU打满快要冒烟了。组里一个刚入职的小兄弟出的主意是:"要不加台配置更高的服务器?"

那天晚上我悟出一个道理:给一辆轮胎没气的车换大排量发动机,能跑快才怪。真正的问题几乎永远是缓存——但不是你从StackOverflow搜到的第一种解法那种缓存。

下面是真正有效的方案,按效果排序。

1. API缓存的二八定律

没人会告诉你的是:80%的流量集中在20%的接口上。先把这些接口找出来。

我用一个简单的nginx日志分析脚本,把请求量最高的接口扒了出来:

awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

结果一出来,我的/api/products/api/products/{id}两个接口干掉了73%的请求量。就从这儿开刀。

2. 响应缓存——最简单有效的第一步

最简单的缓存就是HTTP响应缓存。如果数据不是每秒都在变,把整个响应缓存起来。

优化前(3.2秒):

@app.get("/api/products")
def get_products(): products = db.query("SELECT * FROM products JOIN categories JOIN inventory...") return format_response(products) # 3.2秒都在表关联和序列化上了

优化后(45毫秒):

import functools
from cachetools import TTLCache cache = TTLCache(maxsize=1000, ttl=300) # TTL 5分钟 @app.get("/api/products")
def get_products(): cache_key = "products:list" if cache_key in cache: return cache[cache_key] products = db.query("SELECT * FROM products JOIN categories JOIN inventory...") response = format_response(products) cache[cache_key] = response return response

就这一下,响应时间从3.2秒掉到45毫秒。用户群里瞬间安静了。

但这里有个坑——我第二天凌晨3点又收到了一条bug报告:

数据变化时必须清理缓存。 我忘了这茬。有个商品改了价格,但用户看到的还是5分钟前的缓存价格。

修复方案:

@app.put("/api/products/{id}")
def update_product(id: int, data: dict): db.update("products", id, data) cache.pop("products:list", None) # 清理列表缓存 cache.pop(f"products:{id}", None) # 清理单个商品缓存 return {"status": "ok"}

3. 数据库查询缓存——重头戏

响应缓存很好用,但如果不同调用方需要不同格式的数据怎么办?这时候就要在查询层面做缓存。

我用的是Redis(现在阿里云、腾讯云都有托管服务,部署很方便)。核心思路是这样的:

import json
import redis redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True) def cached_query(query_key: str, ttl: int = 600): """ 把函数结果缓存到Redis的装饰器 """ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): cached = redis_client.get(query_key) if cached: return json.loads(cached) result = func(*args, **kwargs) redis_client.setex(query_key, ttl, json.dumps(result)) return result return wrapper return decorator @cached_query("products:featured", ttl=600)
def get_featured_products(): return db.query("SELECT * FROM products WHERE featured = true ORDER BY score DESC")

这一层又砍掉了200毫秒的平均响应时间。

4. CDN层——半静态数据的加速器

如果你的接口返回的是变化频率很低的数据——商品详情、配置信息、参考数据之类的——在前面加一层CDN。

我的nginx配置:

location /api/products/ { proxy_pass http://backend; proxy_cache my_cache; proxy_cache_key "$scheme$request_method$host$request_uri"; proxy_cache_valid 200 5m; proxy_cache_valid 404 1m; add_header X-Cache-Status $upstream_cache_status;
}

X-Cache-Status这个响应头是调试神器。HIT说明命中缓存,MISS说明走了后端。如果全是MISS,那你的缓存根本没生效。

5. 缓存雪崩防护

当一个热key过期了,100个请求同时打进来查数据库——这就是缓存雪崩。我的处理方式:

import time def get_with_stampede_protection(key, ttl, loader): cached = redis_client.get(key) if cached: return json.loads(cached) # 用锁确保只有一个请求去刷新缓存 lock_key = f"lock:{key}" if redis_client.setnx(lock_key, "1"): redis_client.expire(lock_key, 30) try: result = loader() redis_client.setex(key, ttl, json.dumps(result)) return result finally: redis_client.delete(lock_key) # 其他请求短暂等待后重试 time.sleep(0.1) return get_with_stampede_protection(key, ttl, loader)

最终效果

三层缓存全部上线后,数据是这样的:

指标 优化前 优化后
平均响应时间 3.2秒 180毫秒
P99延迟 8.7秒 450毫秒
数据库CPU 78% 23%
每秒处理请求数 120 850

响应时间降低了94%。文章标题我都说保守了。

如果重来一次,我会这么做

  1. 先上监控。 我之前花了两周优化了一批没人访问的接口。一定要先量清楚哪些接口是热点,再动手缓存。

  2. TTL要按数据特性来定,别拍脑袋。 商品价格一天变一次,5分钟缓存没问题。用户登录态这种实时变化的,干脆就别缓存。

  3. 从第一天就加上雪崩防护。 第一次遇到热key过期撞上流量高峰,数据库差点扛不住。这个坑别踩。

  4. 监控缓存命中率。 没法度量的优化就是耍流氓。加个Prometheus指标,记录缓存命中和未命中的次数,一眼就能看出缓存策略有没有真正起作用。

总结

缓存是成本最低的性能优化手段。不需要重构整个代码库,不需要买更多服务器,需要的只是搞清楚数据怎么变的、变得多快、谁在用。

先从流量最高的接口入手,一层层加缓存,每加一层都看效果。还有最重要的一点——数据变化时记得清理缓存。

凌晨两点被报警叫醒的时候,你会感谢现在的自己的。