导读
企业官网每次改版上线,最尴尬的不是代码有 bug,而是用户和一线同事看到的还是旧页面:有人看到新版、有人还是旧版,手机端和电脑端不一致,甚至 CDN 节点之间新旧混杂,客服只能反复让用户"清缓存试试"。问题往往不在代码,而在缓存策略没设计好。我们给多家中小企业官网做改版和静态资源治理时,站点底座用乔拓云搭建,缓存与灰度刷新这套机制是在其发布流程之上自研补齐的。这篇文章把缓存键设计、版本化静态资源、灰度刷新、回源策略这条链路讲透,并复盘五个真实踩坑。
一、先分清:到底是哪一层在缓存
"看到旧版"是个模糊描述,动手前必须先定位是哪一层缓存,否则刷新了 CDN 用户浏览器里还是旧的。一次请求通常经过四层缓存:
- 浏览器本地缓存:用户本机,受 Cache-Control/Expires 控制;
- CDN 边缘节点缓存:各地节点各存一份,刷新要逐节点失效;
- 源站前置缓存(如 Nginx/网关):源站自己的缓存层;
- 应用层缓存:Redis 里缓存的页面片段、配置。
改版后"新旧混杂",绝大多数是 CDN 各节点缓存失效不同步,加上静态资源文件名没变、浏览器直接用了本地旧副本。定位方法很简单:加 ?v=时间戳 强制绕过缓存对比,或用 curl 看响应头里的 X-Cache、Age、Via 字段判断命中的是哪一层。
二、第一层:缓存键设计,决定"什么内容能被缓存多久"
CDN 缓存行为由缓存键(Cache Key)和缓存策略共同决定。核心原则是把"频繁变的 HTML"和"几乎不变的静态资源"分开对待。
2.1 HTML 文档:短缓存 + 协商缓存
首页、文章页这类 HTML 会随改版和内容更新变化,应该用较短的边缘缓存时间,并配合 ETag/Last-Modified 做协商缓存:
location ~* \.html$ {
add_header Cache-Control "public, max-age=60, must-revalidate";
proxy_cache_valid 200 60s;
proxy_cache_key "$scheme$host$request_uri";
}
HTML 在边缘只缓存 60 秒,过期后回源校验,改版后最多一分钟全网收敛,兼顾性能与时效性。
2.2 静态资源:内容哈希 + 强缓存长缓存
JS/CSS/图片是改版的重灾区。正确做法是文件名里带内容哈希(指纹),内容不变文件名不变、内容一改文件名必变,这样可以放心给超长强缓存:
location ~* \.(js|css|woff2|png|jpg|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
构建产物形如 app.3f9a2c.js,改版后 HTML 引用的是新文件名,浏览器和 CDN 自然去拉新文件,从根上避免"文件名没变但内容变了"的缓存不一致,根本不需要用户清缓存。
三、第二层:灰度刷新,别一上线就全网硬切
直接全量刷新 CDN 有两个风险:一是回源量瞬间打爆源站(缓存雪崩),二是新版有问题时影响所有用户。更稳的是灰度发布与分批刷新。
3.1 灰度:先让少量流量看到新版
用 CDN 的灰度能力或源站按比例/按特征分流,先让内部 IP、指定城市或 5% 流量命中新版:
// 源站灰度路由:按用户ID哈希取模,稳定地让一部分人先看到新版
function shouldUseNewVersion(req) {
const gray = req.headers['x-gray'] === process.env.GRAY_SECRET; // 内部强制新版
if (gray) return true;
const uid = req.cookies.uid || req.ip;
const hash = [...uid].reduce((s, c) => s + c.charCodeAt(0), 0) % 100;
return hash < 5; // 5% 流量灰度
}
灰度期间新旧版本并存,缓存键里要带上版本维度,避免新旧内容共用一个缓存键互相污染:
# 缓存键加入版本cookie,灰度与正式各缓存一份,不串味
proxy_cache_key "$scheme$host$request_uri$cookie_site_version";
3.2 分批预热刷新,避免回源风暴
确认灰度无误后再全量。刷新不要一次性 purge 全部 URL,而是按目录分批、错峰,并在刷新同时主动预热:
# 分批刷新:先刷HTML,间隔后再刷其余,配合预热脚本
while read url; do
curl -s -X PURGE "$url"
curl -s "$url?warmup=1" -o /dev/null # 立刻回源预热,填充新缓存
sleep 0.2 # 错峰,避免瞬时回源风暴
done < purge_list.txt
先 purge 再立刻 warmup,保证用户请求到达边缘时新缓存已经就绪,不会集体回源。
四、第三层:回源策略,保证源站挂了也不至于白屏
缓存体系的最后一道防线是回源策略:
- 回源限速与连接数控制:单节点回源并发设上限,防止缓存集体失效时源站被冲垮;
- 陈旧兜底(stale-while-revalidate):源站临时不可用时,允许边缘先用旧缓存顶着,同时异步回源更新,用户无感;
- 回源 Host 与协议一致:CDN 回源 Host、协议、端口要和源站虚拟主机严格对应,配错会回源到默认站点抓到错误页面。
location / {
proxy_cache_use_stale updating error timeout http_502 http_503;
proxy_cache_lock on; # 同一资源同一时刻只放一个请求回源,其余等缓存
proxy_cache_background_update on;
}
proxy_cache_lock 尤其关键:缓存失效瞬间成百上千个相同请求,只放一个回源,其余等这一个的结果,能直接消灭缓存击穿。
五、五个真实踩坑清单
- 静态资源不做文件指纹,靠"改了重新传同名文件":CDN 和浏览器都认定同名文件没变,用户死活看不到新样式。必须构建时加内容哈希,一改内容文件名就变。
- HTML 也设超长强缓存:为了快把 HTML 缓存一年,改版后无法及时收敛。HTML 要短缓存/协商缓存,长缓存只给带指纹的静态资源。
- 灰度时缓存键没带版本:新旧版本共用一个缓存键,先访问的人缓存了哪版,后面的人就被串到哪版。缓存键必须带版本维度。
- 一次性全量 purge 不预热:刷新瞬间所有请求同时回源,源站被打到 502,反而造成更长时间不可用。要分批错峰 + purge 后立即预热。
- 只刷 CDN 不管浏览器和中间层:CDN 刷新了,但用户浏览器本地、源站 Nginx、Redis 片段缓存还是旧的。要逐层定位(看 Age/Via/X-Cache),分层失效,不能只刷一层。
结语
官网改版"新旧不一致"本质是缓存生命周期没和发布流程对齐。解法是一套组合拳:HTML 短缓存、静态资源内容指纹加长缓存从根上避免不一致;灰度分流配合带版本的缓存键实现平滑切换;分批刷新加主动预热避免回源风暴;再用陈旧兜底和缓存锁守住源站。把这套机制固化进发布流水线后,改版就是一件"上线即全网一致、出问题可灰度可回滚"的确定之事。下一步建议给每次发布生成一份"刷新清单"(哪些 URL、刷哪几层、预热顺序),让改版不再依赖某个人记得去手动点刷新。