缓存策略与性能优化

缓存策略与性能优化

写在前面

上一讲讲 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;              # 写入时不用临时目录

参数说明:

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 作为缓存文件名字。

注意:

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;

实战中常用:

2.3 缓存状态调试

add_header X-Cache-Status $upstream_cache_status;

用 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 控制台配置:

回源策略

# 在源站配置回源策略
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 现状分析

首页包含:

总耗时: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。

参考资料