缓存策略与性能优化
缓存策略与性能优化
写在前面
上一讲讲 HTTPS,这一篇讲性能。性能优化最有效的手段永远是缓存——不是更快的代码,不是更好的硬件,是把已经算好的结果存起来,下次直接用。
本篇会讲 Nginx 提供的所有缓存手段:proxy_cache 代理缓存、fastcgi_cache 后端缓存、客户端浏览器缓存,以及怎么和 CDN 配合。最后实战一个把首页 TTFB 从 800ms 压到 60ms 的完整案例。
一、缓存的层级
一个 HTTP 请求,从浏览器到后端,经历的缓存层:
[Browser Cache]
↓ miss
[CDN Edge]
↓ miss
[Nginx proxy_cache] ← 本篇重点
↓ miss
[Application Cache] ← Redis / Memcached
↓ miss
[Database Cache] ← MySQL Query Cache / Buffer Pool
↓ miss
[Database]
每一层缓存命中都能省一次网络/计算。实战中我们用了 4 层(浏览器 + CDN + Nginx + Redis)。
二、proxy_cache 代理缓存
2.1 基础配置
# http 块顶部
proxy_cache_path /var/cache/nginx/api \
levels=1:2 \
keys_zone=api_cache:10m \
max_size=10g \
inactive=60m \
use_temp_path=off;
server {
location / {
proxy_pass http://backend;
proxy_cache api_cache;
proxy_cache_valid 200 304 10m; # 200/304 缓存 10 分钟
proxy_cache_valid any 5s; # 其他状态缓存 5 秒
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_min_uses 1; # 被请求 1 次就缓存
add_header X-Cache-Status $upstream_cache_status;
}
}
2.2 关键指令详解
proxy_cache_path
proxy_cache_path /var/cache/nginx/api \
levels=1:2 \ # 目录层级
keys_zone=api_cache:10m \ # 共享内存 key 存储
max_size=10g \ # 最大磁盘占用
inactive=60m \ # 不访问 60 分钟后清除
use_temp_path=off; # 写入时不用临时目录
参数说明:
levels=1:2:两级子目录,防止单目录文件过多。如果 URL 哈希用 16 进制 MD5,可以分到 16x256=4096 个目录,单目录最多 256 个文件keys_zone=api_cache:10m:10MB 共享内存,大约存 8 万个 key。如果 keys 超过,LRU 淘汰max_size=10g:缓存总大小上限,超过会按 LRU 淘汰inactive=60m:60 分钟没访问就清除,不等 max_size 满use_temp_path=off:不用临时目录,直接写入最终位置
proxy_cache_valid
proxy_cache_valid 200 304 10m; # 200/304 缓存 10 分钟
proxy_cache_valid any 5s; # 其他状态缓存 5 秒
按 HTTP 状态码分别设置 TTL。必须有这一行,否则缓存不生效。
proxy_cache_key
proxy_cache_key "$scheme$host$request_uri";
缓存键——Nginx 用这个字符串 MD5 作为缓存文件名字。
注意:
- 默认是
$scheme$proxy_host$request_uri,包含 upstream host(多后端时有用) - 包含
$query_string时,每个 query 参数组合都是独立的 key - 用户相关 API 不应该缓存——不能包含 cookie
proxy_cache_min_uses
proxy_cache_min_uses 1;
被请求 N 次才缓存——避免缓存一次性的请求。
高流量 API 设 1(任何请求都缓存),低流量 API 设 5(减少冷数据占空间)。
proxy_cache_bypass / proxy_no_cache
# 这些情况不读缓存(直接打 upstream)
proxy_cache_bypass $http_authorization $cookie_nocache $arg_nocache;
# 这些情况不写缓存
proxy_no_cache $http_authorization $cookie_nocache;
实战中常用:
- 用户登录后才返回的内容:
proxy_no_cache $cookie_session; - 后台预览:
proxy_cache_bypass $arg_preview; - 调试:
proxy_cache_bypass $arg_nocache;(URL 加?nocache=1)
2.3 缓存状态调试
add_header X-Cache-Status $upstream_cache_status;
HIT:缓存命中MISS:缓存未命中,请求 upstreamEXPIRED:缓存过期,重新请求STALE:上游出错,返回过期缓存(配合proxy_cache_use_stale)BYPASS:强制不走缓存REVALIDATED:缓存被 If-Modified-Since / If-None-Match 重新验证
用 curl 看:
curl -I https://api.company.com/v1/products
# HTTP/2 200
# ...
# x-cache-status: HIT
2.4 缓存清理
主动清理某个 URL
# 用第三方模块 nginx-cache-purge
location ~ /purge(/.*) {
allow 127.0.0.1; # 只允许本机
deny all;
proxy_cache_purge api_cache "$scheme$host$1";
}
调用:
curl -X PURGE http://localhost/purge/v1/products/123
批量清理(通过 Lua)
用 OpenResty + Lua 脚本:
-- 遍历 keys_zone 里的 key,删除匹配的
local cache = ngx.shared.api_cache
local keys = cache:get_keys(0)
for _, key in ipairs(keys) do
if string.match(key, "/v1/products") then
cache:delete(key)
end
end
通过文件系统清理
# 直接删除缓存文件
rm -rf /var/cache/nginx/api/*
⚠️ 会导致这一瞬间所有请求 cache miss,但不会出错。
三、FastCGI 缓存
PHP 应用常用 FastCGI 缓存。配置类似:
fastcgi_cache_path /var/cache/nginx/php \
levels=1:2 \
keys_zone=php_cache:10m \
max_size=5g \
inactive=30m;
server {
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_cache php_cache;
fastcgi_cache_valid 200 10m;
fastcgi_cache_key "$scheme$host$request_uri";
fastcgi_cache_bypass $cookie_session;
add_header X-Cache-Status $upstream_cache_status;
}
}
注意:FastCGI 缓存对动态内容(数据库查询)加速效果最好,对纯静态文件(.css .js)效果有限。
四、浏览器缓存
4.1 Expires(老)
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
老浏览器兼容用 Expires,新浏览器用 Cache-Control,都设上。
4.2 Cache-Control(新)
| 值 | 含义 |
|---|---|
public |
任何缓存都可以存 |
private |
只有浏览器可以存,CDN 不能 |
no-cache |
必须重新验证(304 可能) |
no-store |
不缓存任何东西 |
max-age=N |
缓存 N 秒 |
s-maxage=N |
CDN 缓存 N 秒(覆盖 max-age) |
immutable |
永不变(指纹文件名) |
must-revalidate |
过期后必须验证 |
4.3 静态资源的最佳实践
带 hash 的文件名 + 长缓存:
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
# /static/logo.a1b2c3d.png - 文件名带 hash,可以永久缓存
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* \.(js|css)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# HTML 不缓存或短缓存
location / {
add_header Cache-Control "no-cache, must-revalidate";
}
4.4 缓存验证(304)
location ~* \.(jpg|css|js)$ {
etag on; # 启用 ETag(默认)
if_modified_since exact; # 精确匹配 If-Modified-Since
}
服务器返回 ETag 头,浏览器下次请求带 If-None-Match。如果文件没变,返回 304 Not Modified,不传 body——节省大量带宽。
五、CDN 配合
5.1 Nginx + CDN 架构
用户 → CDN Edge → Nginx (源站) → 后端
5.2 关键配置
源站验证 CDN 回源
# 只允许 CDN IP 访问源站
allow 1.2.3.0/24; # CDN 厂商的 IP 段
allow 5.6.7.0/24;
deny all;
或者用 CDN 的回源认证头:
# 阿里云 CDN
set $cdn_auth $http_aliyun_cdn_real_ip;
if ($cdn_auth = "") {
return 403;
}
CDN 缓存配置
CDN 控制台配置:
- 静态资源:30 天
- HTML:5 分钟
- API:不缓存(由源站控制)
回源策略
# 在源站配置回源策略
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $http_x_real_ip; # 传递 CDN 给的真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://backend;
}
5.3 缓存层级控制
# HTML - 短缓存(浏览器可能更新)
location = / {
add_header Cache-Control "no-cache, must-revalidate";
}
# API - 浏览器不缓存,CDN 可以缓存
location /api/ {
add_header Cache-Control "private, max-age=0, s-maxage=60";
}
# 静态资源 - 浏览器长缓存
location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
六、实战:首页 TTFB 从 800ms 到 60ms
6.1 现状分析
首页包含:
- 个人信息(从 user 表)
- 推荐商品列表(从商品表 + Redis 缓存)
- 购物车数量(从 Redis)
- 公告(从 CMS API)
- 推荐位(从算法服务)
总耗时:800ms。
主要耗时在 CMS API 和算法服务的远程调用。
6.2 第一轮:加 Redis 应用层缓存
后端先查 Redis,Redis miss 才查 DB / 调远程 API。
async function getHomePage(userId: string) {
const cacheKey = `home:${userId}`;
let data = await redis.get(cacheKey);
if (data) return JSON.parse(data);
data = await fetchFromBackend();
await redis.setex(cacheKey, 300, JSON.stringify(data)); // 5 分钟
return data;
}
TTFB 从 800ms 降到 350ms。
6.3 第二轮:Nginx proxy_cache 整页缓存
proxy_cache_path /var/cache/nginx/home \
levels=1:2 \
keys_zone=home_cache:50m \
max_size=5g \
inactive=10m;
location = / {
proxy_pass http://backend;
proxy_cache home_cache;
proxy_cache_valid 200 5m;
proxy_cache_key "$scheme$host$request_uri";
# 但用户登录后内容不一样 - 用 cookie 区分
proxy_cache_bypass $cookie_session;
add_header X-Cache-Status $upstream_cache_status;
}
匿名用户的首页缓存 5 分钟。TTFB 从 350ms 降到 80ms。
6.4 第三轮:CDN 缓存
匿名首页内容其实很少变,推给 CDN:
location = / {
proxy_pass http://backend;
proxy_cache home_cache;
proxy_cache_valid 200 1h; # 源站缓存 1 小时
# 配合 CDN:让 CDN 也缓存
add_header Cache-Control "public, s-maxage=3600";
proxy_cache_bypass $cookie_session;
}
CDN 命中后,用户访问直接从最近的 CDN 节点返回。TTFB 从 80ms 降到 60ms(CDN 节点网络延迟)。
6.5 第四轮:ESI(Edge Side Includes)
但首页有部分内容需要个性化(用户名、购物车数量)。用 ESI 把页面分成片段:
<html>
<body>
<!-- 公共部分 - 缓存 -->
<esi:include src="/widgets/header"/>
<!-- 用户部分 - 不缓存 -->
<esi:include src="/widgets/user-info"/>
<!-- 公共部分 - 缓存 -->
<esi:include src="/widgets/recommend"/>
<!-- 用户部分 - 不缓存 -->
<esi:include src="/widgets/cart-count"/>
</body>
</html>
需要 OpenResty 支持。TTFB 仍是 60ms,但个性化内容单独异步加载。
6.6 最终效果
| 阶段 | TTFB | 优化手段 |
|---|---|---|
| 原始 | 800ms | 无 |
| 加 Redis | 350ms | 应用层缓存 |
| 加 Nginx proxy_cache | 80ms | 整页缓存 |
| 加 CDN | 60ms | CDN 边缘缓存 |
| 加 ESI | 60ms | 个性化与缓存分离 |
10 倍以上的性能提升,没有一行代码优化,只是加了缓存。
七、本篇小结
| 缓存类型 | 适用 | TTL |
|---|---|---|
proxy_cache |
反向代理内容 | 分钟到小时 |
fastcgi_cache |
PHP 动态内容 | 分钟 |
| 浏览器缓存 | 静态资源 | 天到年 |
| CDN | 全球分发 | 小时到天 |
| 应用层(Redis) | 高频读 | 秒到分钟 |
| 关键点 | 说明 |
|---|---|
proxy_cache_key |
不要包含用户标识 |
proxy_cache_valid |
必须设,否则不缓存 |
| 用户内容 | 用 cookie 跳过缓存 |
| 静态资源 | 带 hash + immutable |
| CDN | 用 s-maxage 单独控制 |
下一篇讲 安全加固实战——防 CC、防注入、限流、ModSecurity。