改了配置,线上却像没改过
个人站长最容易丧失信心的一类问题就是"我明明改了配置/发了新文章/换了模板,本地预览完全正确,线上刷新却一直是老的"。你反复检查源文件,确认改动确实写进去了,甚至重启了服务,可浏览器和第三方工具看到的还是旧内容。这种时候,问题往往不在应用本身,而在层层叠加的缓存上。一次请求从用户浏览器到最终落地的源站,中间可能隔着浏览器缓存、CDN 边缘缓存、Nginx 的 proxy_cache、应用自身的页面缓存,任何一层命中都会让改动"消失"。
要定位这类问题,第一步是搞清楚"到底是谁缓存了"。不要依赖浏览器刷新按钮,也不要用 Ctrl+F5 就以为清干净了——强制刷新只影响当前这个浏览器,CDN 和反向代理那一层的缓存纹丝不动。真正可靠的方法是在命令行下用 curl 观察响应头。
# 看响应头里是谁在提供内容
curl -sI "https://example.com/some-page" | grep -Ei 'age|cache-control|x-cache|cf-cache|expires|etag|via|server'
# 典型输出解读
# age: 842 -> 该响应在缓存里已经放了 842 秒
# x-cache: HIT -> 命中了某层缓存
# cf-cache-status: HIT -> 命中了 Cloudflare 边缘缓存
# cache-control: max-age=86400 -> 告诉所有层可以缓存一天关键看 Age 头。如果 Age 大于 0,说明响应来自缓存,而且那个数字就是它已经被缓存了多久。Age 很大而你的改动是刚做的,那就基本可以锁定是缓存问题,而不是程序没生效。
先给缓存分层,再动手清理
缓存之所以难缠,是因为它不是一个东西,而是好几层。按从用户到源站的顺序理一遍:
浏览器缓存受 Cache-Control 和 Expires 控制,一旦资源被标成 immutable 且 max-age 很长,在过期前浏览器根本不会发起请求,服务器端再怎么改都没用。这类问题只能靠"改文件名"(也就是加内容哈希)来解决,别无他法。
CDN 边缘缓存是个人站长最常遇到的元凶。以 Cloudflare 为例,静态资源(图片、CSS、JS)默认就会被缓存,哪怕你的源站没设任何缓存头。这时候在后台点"Purge Everything"能立刻生效,但要注意 purge 是按 URL 或标签来的,如果你改了模板导致所有页面都变,就得全量清理,而不是只清首页。
Nginx 的 proxy_cache / fastcgi_cache是应用层缓存。很多个人站为了扛住流量会给 PHP 加上 fastcgi_cache,一旦开了,动态页面就会被当成静态文件缓存起来。fastcgi_cache_key 如果没把 Cookie、语言、设备类型这些维度加进去,就会出现"登录用户看到未登录页面"的经典事故。
# Nginx fastcgi_cache 的最小正确配置
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=PHP:100m inactive=60m max_size=1g;
server {
location ~ \.php$ {
fastcgi_cache PHP;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_valid 404 1m; # 404 也缓存,防止穿透打爆后端
fastcgi_cache_bypass $skip_cache; # 下面用 map 控制
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
map $request_uri $skip_cache {
default 0;
~*/wp-admin/ 1; # 后台不缓存
~*/preview/ 1;
~*\?s= 1; # 搜索页不缓存
}
map $http_cookie $skip_cache_cookie {
default 0;
~*wordpress_logged_in 1; # 登录用户不缓存
}这段配置里最值钱的是 add_header X-FastCGI-Cache $upstream_cache_status; 这一行。它会把 HIT / MISS / BYPASS / EXPIRED 直接写进响应头,排查时一眼就能看出当前请求是命中还是回源,比翻日志快得多。建议所有开了缓存的站点都加上这个调试头。$upstream_cache_status 的四个值含义也要记清楚:MISS 是缓存里没有、回源了;HIT 是直接命中;BYPASS 是被 fastcgi_cache_bypass 规则跳过;EXPIRED 是缓存过期后重新拉取。
缓存穿透、击穿与雪崩的实用对策
缓存给便利,也带来三类经典故障。穿透是指请求查询一个根本不存在的数据,缓存永远不命中,每次都打到数据库。对个人站来说,最常见的形式就是爬虫疯狂访问不存在的 URL,把 PHP-FPM 打满。对策是把 404 也缓存(上面配置里的 fastcgi_cache_valid 404 1m; 就是干这个的),让不存在的页面在一分钟内直接由 Nginx 返回,不进后端。
击穿是某个热点 key 过期的瞬间,大量并发同时回源。个人站通常并发不高,但有一种情况很典型:首页或某篇爆款文章的缓存同时过期,恰好来了一波推广流量,几十个请求同时重建缓存,把数据库打满。对策是给热门页面用较长的缓存时间,或者引入"逻辑过期"——缓存不设 TTL,由后台任务异步刷新。
雪崩是所有缓存同时失效。避免给大批 key 设置完全相同的过期时间,加一点随机抖动(比如 600±60 秒),就能把压力摊开。
一个可复用的缓存排查流程
把上面的经验整理成一套可执行的流程,下次遇到"改动不生效"直接照着走:
第一步,curl -sI 看响应头,关注 Age、x-cache、cf-cache-status、X-FastCGI-Cache,确认是缓存命中还是源站直出。第二步,加一个随机 query 参数(?_=123456)再请求一次。如果加了随机参数就拿到了新内容,说明是缓存问题,而且缓存的 key 包含了 query string;如果加了参数还是旧内容,说明问题在更上层(CDN 忽略了 query,或者浏览器强缓存)。第三步,如果确认是 CDN,去后台 purge 对应 URL。第四步,如果确认是 Nginx 层,find /var/cache/nginx -type f -delete 清空缓存目录,或者用 proxy_cache_purge 模块精准清除。第五步,如果确认是应用层缓存,检查是否有 opcache、Redis、Memcached 或者框架自带的对象缓存,逐个清。
还有两个容易被忽略的细节。其一,Vary 头缺失会导致错误的缓存命中:如果页面内容会根据 Accept-Encoding(gzip/br)或 User-Agent 变化,而缓存层没有正确设置 Vary,就可能把 gzip 版本的内容发给不支持压缩的客户端,或者把移动端页面发给桌面端。其二,缓存清除是"广播式"的:如果你有多个 CDN 节点或多个 Nginx 实例,purge 请求必须保证送达每一个节点,否则会出现"刷新几次有时新有时旧"的玄学现象——这本质上就是不同节点缓存状态不一致造成的。
最后强调一个观念:缓存不是需要消灭的东西,而是需要管理的东西。个人站要扛住搜索引擎的抓取和偶尔的流量波动,缓存几乎是必需品。正确的姿势是主动设计缓存策略——静态资源用长 TTL 加内容哈希,动态页面用短 TTL 加精准的 bypass 规则,再配上 $upstream_cache_status 调试头随时可观测。这样既能享受缓存的性能红利,又不会在需要改动时抓瞎。
CDN 缓存命中的判定与 Cache-Control 的正确写法
想让缓存按你的意愿工作,核心是把 Cache-Control 写对。这句话听起来简单,实际上很多站长栽在「以为写了就生效」上——因为 CDN 往往只认自己后台的规则,会忽略你源站发的某些头。以 Cloudflare 为例,它默认对静态扩展名(.css/.js/.jpg/.woff2 等)启用边缘缓存,而 HTML 页面默认不缓存;一旦你通过 Page Rule 或者 Cache Rule 把 HTML 也纳入缓存,源站发的 Cache-Control: no-store 在某些套餐下依然会被 CDN 覆盖。
# 推荐的分类缓存策略
# 1) 带内容哈希的静态资源:一年不过期,且声明不可变
location ~* \.(css|js|woff2|jpg|png|webp|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# 2) 首页与列表页:短缓存 + 允许 CDN 缓存,但要求回源校验
location = / {
add_header Cache-Control "public, max-age=300, s-maxage=1800, stale-while-revalidate=60";
}
# 3) 用户相关接口:绝对不缓存
location /api/user {
add_header Cache-Control "private, no-store, must-revalidate";
}
# 4) 需要更新的 HTML:用 ETag 做协商缓存,不要硬缓存
location ~* \.html$ {
etag on;
add_header Cache-Control "public, max-age=0, must-revalidate";
}这里有几个关键词要理解。max-age 是给浏览器看的,s-maxage 是给共享缓存(CDN、代理)看的,两者可以不同——这正是「让 CDN 缓存久一点、但浏览器别缓存太久」的实现方式。stale-while-revalidate 表示缓存过期后可以先把旧内容返回给用户、同时后台异步去源站刷新,这是兼顾速度和新鲜度的关键指令。immutable 则是告诉浏览器「这个 URL 的内容永远不变,别再发校验请求了」,只有在文件名带内容哈希时才该用——文件名不变的内容绝不能标 immutable,否则用户永远拿不到更新。
还有一个判断缓存是否命中的实用技巧:看两次请求的响应时间差。缓存命中通常只有几十毫秒,回源则可能几百毫秒到数秒。用 curl -w 把各阶段耗时打出来,连续请求两次对比,比猜靠谱。
curl -s -o /dev/null -w 'dns=%{time_namelookup}s connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
"https://example.com/page"
# 连续两次,第二次明显快 -> 命中缓存缓存与 SEO:别让搜索引擎看到旧内容
缓存对 SEO 有直接影响,这一点常被忽视。搜索引擎爬虫也是一个「客户端」,它拉到的页面同样可能是缓存版本。如果缓存策略过激,会出现两种坏情况:一是爬虫长期拿到旧页面,你的内容更新对搜索排名毫无帮助;二是缓存层返回了错误的响应码,比如后端临时 503 被缓存了 10 分钟,结果爬虫连续收到 503,直接在 Search Console 里报「服务器错误」,拖慢整站抓取。
对策有三条。第一,给错误响应设很短的缓存时间(前面配置里的 fastcgi_cache_valid 404 1m; 同理,5xx 最好设成 0s 完全不缓存),避免故障被缓存放大。第二,发布新文章时主动清理相关缓存,包括文章页、首页、分类页、RSS 和 sitemap——很多站长只清了文章页,忘了首页还在展示旧的「最新文章」列表,看起来就像发布失败。第三,用缓存刷新 API 把它做成发布流程的一部分,而不是手动去后台点。Cloudflare、又拍云、七牛这类服务都提供 purge 接口,可以在发布脚本末尾调用,保证内容和缓存同步更新。
# Cloudflare 单 URL 刷新示例(发布后调用)
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"files\":[\"https://example.com/$NEW_CID.html\",\"https://example.com/\"]}"把这套流程跑顺之后,你会彻底告别「改了不生效」和「发了没反应」这类内耗。缓存的本质是空间换时间,只要你能随时知道「谁在缓存、缓存了多久、怎么清掉」,它就是最便宜的加速手段,而不是玄学故障源。