美国服务器部署网站时,页面缓存与对象缓存如何设置失效规则

在美国服务器部署网站时,建议把缓存分成四个层级管理:浏览器缓存、边缘缓存、页面缓存和对象缓存。公开且变化不频繁的页面可以使用页面缓存,数据库查询结果、配置或内容对象适合放入对象缓存;登录态、购物车、后台页面和带敏感参数的请求则不应进入共享页面缓存。
开始前应确认网站运行方式、反向代理软件、应用使用的会话 Cookie、缓存目录权限以及 Redis 等对象缓存服务的连接方式。以下示例以 Linux + Nginx 反向代理应用服务为例,假设应用监听 127.0.0.1:8080。如果实际环境使用 PHP-FPM、其他代理软件或不同的 Cookie 名称,应先替换对应配置,不能直接覆盖现有生产配置。
先划分缓存对象和失效方式
缓存失效规则不能只按“缓存多久”决定,还要结合内容是否公开、是否带查询参数以及数据更新时能否触发清理。
| 缓存层级 | 适合缓存的内容 | 推荐失效方式 | 不应缓存的内容 |
|---|---|---|---|
| 浏览器缓存 | 带内容指纹的 CSS、JS、图片、字体 | 文件名变更、版本号变更 | 用户私有页面 |
| 边缘缓存 | 公开 HTML、公开 GET 接口、静态资源 | 按 URL 或标签清理,TTL 作为兜底 | 登录页、后台、购物车 |
| 页面缓存 | 首页、文章页、公开列表页 | 发布后清理当前页及关联页 | 带登录 Cookie、预览、搜索结果 |
| 对象缓存 | 文章数据、分类列表、配置、聚合查询结果 | 写入成功后删除关联 Key 或切换命名空间 | 未隔离用户身份的私密数据 |
| 查询缓存 | 允许缓存的固定查询组合 | 只保留参数白名单,变更后按组合清理 | 任意拼接的搜索、筛选和敏感参数 |
一个实用原则是:页面缓存解决重复生成 HTML 的问题,对象缓存解决重复读取数据的问题。页面缓存命中后,应用和 Redis 可能都不会被访问;页面缓存失效后,应用才能通过对象缓存减少数据库压力。
第一步:检查请求是否具备缓存条件
先从响应头和请求特征入手,不要一开始就把所有 URL 设置为公共缓存。
在可以访问网站的终端执行:
curl -sS -D /tmp/home.headers -o /dev/null https://example.com/
cat /tmp/home.headers
重点检查以下内容:
- 请求是否为
GET或HEAD; - 响应是否包含
Set-Cookie; - 是否存在
Cache-Control: private、no-store或no-cache; - 是否依赖
Authorization请求头; - URL 查询参数是否改变页面内容;
- 页面是否会根据 Cookie 显示用户名、购物车或个性化推荐。
公开页面通常可以从短 TTL 开始,例如页面缓存设置为几十秒到几分钟,再根据发布频率调整。带登录态的页面应由应用返回:
Cache-Control: private, no-store
公开 HTML 如果需要被边缘缓存,可以采用类似的策略:
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=30
这里的 s-maxage 只对共享缓存有意义,max-age=0 可以避免浏览器长时间保留旧 HTML。具体时间应根据内容更新频率和可接受的旧内容时间调整,不能把上述数值当成所有网站的固定标准。
第二步:配置页面缓存,只缓存明确的公开请求
在 Nginx 配置中,缓存区和 map 应放在 http 上下文,不能直接放在 server 或 location 中。先查看当前完整配置,确认已有的缓存区、上游服务和配置文件路径:
sudo nginx -T | less
下面是一个页面缓存示例。它会跳过非 GET/HEAD 请求、带身份认证的请求、包含会话类 Cookie 的请求、预览请求和搜索请求。
# 放在 http {} 内
map $request_method $skip_cache_method {
default 1;
GET 0;
HEAD 0;
}
map $http_authorization $skip_cache_auth {
default 1;
"" 0;
}
# Cookie 名称必须按实际应用调整
map $http_cookie $skip_cache_cookie {
default 0;
~*(^|;\s*)(session|auth|cart|wordpress_logged_in_[^=]*|wp-postpass_[^=]*)= 1;
}
map $arg_preview $skip_cache_preview {
default 0;
"" 0;
1 1;
}
# 仅对已确认不会被页面缓存的搜索参数设置跳过
map $arg_q $skip_cache_search {
default 1;
"" 0;
}
proxy_cache_path /var/cache/nginx/page
levels=1:2
keys_zone=page_cache:20m
inactive=30m
max_size=2g
use_temp_path=off;
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache page_cache;
proxy_cache_methods GET HEAD;
# request_uri 包含查询字符串,默认不会把不同查询组合混为一个页面
proxy_cache_key "$scheme|$host|$request_uri";
proxy_cache_bypass
$skip_cache_method
$skip_cache_auth
$skip_cache_cookie
$skip_cache_preview
$skip_cache_search;
proxy_no_cache
$skip_cache_method
$skip_cache_auth
$skip_cache_cookie
$skip_cache_preview
$skip_cache_search
$upstream_http_set_cookie;
# 仅作为起始策略,具体时间按内容更新频率调整
proxy_cache_valid 200 301 60s;
proxy_cache_valid 404 10s;
# 同一个页面首次回源时,减少并发请求同时击穿缓存
proxy_cache_lock on;
# 便于验证,正式环境也可以保留,但应确认不会暴露内部信息
add_header X-Page-Cache $upstream_cache_status always;
}
}
这段配置有几个关键边界:
proxy_cache_key必须包含会影响页面内容的参数。不能为了提高命中率而盲目删除全部查询字符串。- Cookie 名称必须与实际应用一致。只要应用通过其他 Cookie 控制登录态,就要加入跳过规则。
Set-Cookie响应不应进入公共页面缓存,否则可能把用户状态写入共享缓存。proxy_cache_valid只是时间兜底。内容发布后仍应主动清理受影响的页面。- 不要使用
proxy_ignore_headers强行忽略应用返回的private、no-store等响应头,除非已经确认该响应确实是公开内容。
修改前先备份实际被包含的配置文件,确认 Nginx 运行用户和缓存目录权限:
sudo nginx -T > /tmp/nginx-config-before-cache.txt
ps -o user= -C nginx | sort -u
sudo nginx -t
在使用 systemd 的系统上,检查通过后再平滑加载:
sudo systemctl reload nginx
如果 nginx -t 失败,不要执行 reload。先根据报错恢复备份文件或修正上下文位置。
第三步:为查询参数建立白名单
查询参数是页面缓存最容易出错的地方。应先区分三类参数:
- 内容参数:例如
page=2、category=news,会改变页面内容,应保留在缓存 Key 中; - 跟踪参数:例如某些广告或统计参数,通常不改变页面内容,但只有在应用或边缘层完成规范化后才能从缓存 Key 中删除;
- 用户参数:例如搜索词、筛选条件、预览标记,通常应绕过共享页面缓存。
当前示例直接使用 $request_uri 作为 Key,因此不同查询字符串会生成不同缓存对象,安全性较高,但可能出现大量低命中率缓存。不要直接在 Nginx 中删除所有查询参数。正确做法是:
- 列出真正影响页面内容的参数;
- 只对已确认无业务影响的跟踪参数做规范化;
- 对搜索、预览、导出和用户筛选请求默认绕过页面缓存;
- 用不同参数访问同一页面,检查返回内容是否发生变化。
例如:
curl -sS -D - -o /dev/null 'https://example.com/list?page=2'
curl -sS -D - -o /dev/null 'https://example.com/list?utm_source=test'
curl -sS -D - -o /dev/null 'https://example.com/search?q=cache'
如果第二个请求与第一个请求在业务上完全等价,可以在应用或边缘层进行规范化;如果不能确认,就保留完整查询字符串,不要为了命中率牺牲内容正确性。
第四步:设置静态资源和边缘缓存规则
带内容指纹的静态文件,例如 app.abc123.js、style.2026.css,可以设置较长的浏览器和边缘缓存时间:
Cache-Control: public, max-age=31536000, immutable
前提是每次文件内容变化都会生成新的文件名。如果文件名不变而内容会覆盖更新,就不能使用 immutable,否则用户可能持续使用旧文件。
公开 HTML 可以使用较短的共享缓存时间,并在发布时主动清理。登录、后台、购物车、订单和账户页面则应使用:
Cache-Control: private, no-store
如果网站接入了边缘缓存,失效操作应覆盖实际缓存层级。发布一篇文章时,通常需要处理:
- 新文章的规范 URL;
- 首页或文章列表页;
- 对应分类、标签或分页页;
- 应用中保存文章详情和列表的对象 Key;
- 边缘缓存中对应的公开 URL。
不要只删除数据库中的数据或只清理 Redis。页面缓存仍然存在时,访问者可能继续获得旧 HTML;边缘缓存仍然存在时,请求甚至不会到达美国服务器上的 Nginx。
第五步:为对象缓存设计 Key、TTL 和主动失效
对象缓存应缓存可重复读取、且能够安全重建的数据。Key 必须包含站点、数据类型、对象标识和必要的版本信息,避免不同业务模块互相覆盖。
以支持 phpredis 的 PHP 应用为例,基本逻辑可以写成:
$key = 'site:v1:article:' . (int) $articleId;
$value = $redis->get($key);
if ($value === false) {
$data = loadArticleFromDatabase($articleId);
if ($data !== null) {
$redis->setEx(
$key,
300,
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
$value = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
$article = $value !== false ? json_decode($value, true) : null;
TTL 只应作为兜底规则,数据写入成功后还要主动删除关联对象:
$redis->del(
'site:v1:article:' . (int) $articleId,
'site:v1:article-list:latest',
'site:v1:category:' . (int) $categoryId
);
实际项目中应在数据库事务提交成功后执行失效事件,避免数据库写入失败但缓存已经被删除,或数据库写入成功却没有触发清理。大范围变更时,可以切换命名空间:
site:v1:article:123
site:v2:article:123
应用改用 v2 后,旧 Key 会自然过期,不需要在高峰期遍历删除大量对象。此方法适合配置结构、全站分类规则等批量变更,但会在切换瞬间产生一批回源请求,应配合预热或并发锁。
对象缓存还要防止缓存击穿:
- 对同一 Key 设置短时间互斥锁;
- 锁的 TTL 应小于业务可接受的等待时间;
- 获取锁失败的请求应短暂等待后重读缓存;
- 不要直接删除一个可能已经被其他请求重新占用的锁,应使用客户端提供的安全释放方式;
- TTL 可以加入少量随机偏移,避免大量 Key 同时过期。
Redis 连接检查应使用实际的主机、端口、认证和 TLS 配置。不要在生产环境使用不带认证信息的公共连接,也不要直接执行:
redis-cli FLUSHDB
redis-cli FLUSHALL
这类命令会清除整个数据库或实例中的数据,影响范围远大于单个站点或单个对象。需要清理时,优先使用精确 Key、命名空间切换或应用提供的失效接口。
第六步:按发布动作执行失效
推荐将发布流程固定为以下顺序:
- 写入数据库并确认事务提交成功;
- 删除文章详情、列表、分类等对象缓存;
- 清理页面缓存中的规范 URL 和关联页面;
- 清理边缘缓存中的相同 URL 或缓存标签;
- 用未登录请求访问页面,使缓存重新生成;
- 检查页面内容、响应头和后台数据是否一致。
如果只是静态资源发布,只要文件名带有新的内容指纹,通常不需要清理旧资源;如果覆盖了原文件名,则应清理边缘和浏览器相关缓存,并承担旧文件残留的风险。
结果验证:分别验证命中、绕过和失效
连续访问同一个公开页面,检查页面缓存状态:
curl -sS -D /tmp/page-1.headers -o /dev/null https://example.com/article/123
curl -sS -D /tmp/page-2.headers -o /dev/null https://example.com/article/123
grep -iE 'X-Page-Cache|Cache-Control|Set-Cookie|Age' \
/tmp/page-1.headers /tmp/page-2.headers
在没有其他缓存层干扰时,第一次通常可能显示 MISS,后续请求可能显示 HIT。以下结果具有不同含义:
HIT:页面缓存已命中;MISS:请求回源并写入或尝试写入缓存;BYPASS:命中了跳过条件,例如 Cookie、认证或查询参数;- 长期只有
MISS:可能是 TTL 太短、响应带Set-Cookie、缓存目录无权限或缓存 Key 不稳定; - 登录请求显示
HIT:应立即停止共享页面缓存并检查 Cookie、Authorization 和缓存 Key; - 更新内容后仍为旧内容:通常是页面缓存、边缘缓存或对象缓存中至少有一层未清理。
带会话 Cookie 的请求应单独验证:
curl -sS -D - -o /dev/null \
-H 'Cookie: session=test-session' \
https://example.com/article/123
预期应是绕过公共页面缓存,且响应不能把该用户的 Set-Cookie 写入共享缓存。
对象缓存则检查目标 Key 是否存在、剩余 TTL 是否符合预期。生产环境应通过安全的认证连接执行:
redis-cli --scan --pattern 'site:v1:article:123'
redis-cli TTL 'site:v1:article:123'
发布后应确认目标 Key 已被删除或版本已切换,再检查页面是否重新读取了新数据。不要只看 Redis 命中率,还要核对页面正文、接口响应和数据库最新值。
常见失败处理与回滚
页面始终不命中
先检查响应中的 Set-Cookie、Cache-Control、Vary 和 X-Page-Cache。如果公开页面仍然返回 Set-Cookie,应先从应用层移除不必要的 Cookie,而不是在 Nginx 中强行忽略响应头。再检查缓存目录的磁盘空间和权限:
df -h /var/cache/nginx
sudo nginx -T | grep -n 'proxy_cache_path'
不同用户看到相同的个性化内容
这是高优先级问题。立即临时关闭该页面的共享缓存,清理受影响的边缘和页面缓存,并核对缓存 Key 是否遗漏 Cookie、Authorization 或租户标识。用户私有页面应改为 private, no-store,或者改为只缓存不含用户数据的公共页面片段。
发布后仍显示旧内容
按照“对象缓存 → 页面缓存 → 边缘缓存”的顺序检查失效事件是否执行。不能确认影响范围时,只清理相关 URL;全站清理会造成大量回源请求,应作为最后手段,并提前确认应用和数据库能够承受缓存重新生成。
缓存击穿或回源请求突然增加
检查是否有大量 Key 在同一时间过期、页面缓存是否启用了锁、对象缓存是否有 TTL、发布流程是否一次性清理了过多页面。可以缩小清理范围、加入过期时间随机偏移,并为热点 Key 增加互斥重建机制。
需要紧急回滚缓存配置
先保留当前配置副本,再在对应 location 中临时设置:
proxy_cache off;
或者临时让所有请求绕过并禁止写入缓存:
proxy_cache_bypass 1;
proxy_no_cache 1;
检查配置并平滑加载:
sudo nginx -t
sudo systemctl reload nginx
这只会停止 Nginx 页面缓存,浏览器和边缘缓存仍可能保留旧响应。若涉及错误页面或敏感信息,应同步清理受影响的边缘 URL;不能立即清理时,至少要保持绕过状态,直到旧 TTL 失效。对象缓存回滚优先采用切换回旧命名空间,例如从 site:v2: 切回 site:v1:,不要直接清空整个 Redis 实例。
上线前验收清单
- [ ] 公开页面、用户页面、后台页面已经分开;
- [ ] 页面缓存只接受确认安全的
GET和HEAD请求; - [ ] 会话 Cookie、Authorization、预览和搜索请求能够绕过共享缓存;
- [ ]
proxy_cache_key包含所有会改变页面内容的参数; - [ ] 应用返回的
private、no-store和Set-Cookie没有被强制忽略; - [ ] 对象缓存 Key 包含业务前缀、对象类型和版本;
- [ ] 写入成功后会删除关联对象,并清理规范页面和关联列表页;
- [ ] 静态资源只有在文件名带内容指纹时才使用
immutable; - [ ] 已验证首次访问、重复访问、带 Cookie 访问和发布后访问;
- [ ] 配置文件、对象缓存命名空间和紧急绕过方案均可回滚;
- [ ] Nginx 配置测试通过,缓存目录有足够空间和正确权限。