SSR 页面缓存失效如何设计?如何避免用户看到旧内容?
简化版
SSR 缓存失效要先明确缓存层级:浏览器、CDN、SSR 进程、本地内存、后端缓存都可能缓存页面或数据。常见策略有 TTL 自然过期、主动 purge、按 tag 失效、版本号、stale-while-revalidate 和后台刷新。设计时要在性能和新鲜度之间取舍,关键页面可以短 TTL 或主动失效,内容页可以接受短时间 stale。
详细版
SSR 页面缓存能显著降低 TTFB 和服务端压力,但缓存失效设计不好,会出现商品价格旧、库存旧、登录态串、活动配置不生效等问题。
页面缓存不能只看 URL,还要看语言、地区、设备、登录态、A/B 实验等维度。个性化内容如果混进公共缓存,会产生严重事故。
面试里要把缓存失效讲成“层级 + key + TTL + 主动失效 + 降级兜底”,而不是只说清缓存。
完整版教学
一、先识别缓存层级
浏览器可能缓存 HTML 或静态资源。
CDN 可能缓存 SSR 输出。
SSR 服务内部可能缓存渲染结果或接口数据。
后端服务和数据库前面也可能有缓存。
同一个页面旧内容可能来自任意一层。
二、缓存 key 设计
| 维度 | 是否常见 | 示例 |
|---|---|---|
| URL | 必须 | /product/1 |
| 语言 | 多语言站必须 | zh-CN、en-US |
| 地区 | 有地域内容时需要 | 华东、华北 |
| 登录态 | 个性化页面需要 | anonymous、member |
| 设备 | 差异 HTML 时需要 | mobile、desktop |
| 实验分组 | A/B 时需要 | groupA、groupB |
缓存 key 少了会串内容,太多会降低命中率。
三、TTL 和主动失效
TTL 简单可靠,但内容会在过期前保持旧版本。
主动 purge 更新更及时,但依赖发布系统、CMS 或后端事件通知。
按 tag 失效适合内容关联复杂的页面,例如一个作者更新影响多个列表页。
stale-while-revalidate 允许先返回旧内容,再后台刷新缓存。
缓存失效不是越实时越好,实时性、成本和稳定性要一起平衡。
四、代码示例
export async function renderArticlePage(id: string) {
const cacheKey = `article:${id}:zh-CN`;
const cached = await pageCache.get(cacheKey);
if (cached) return cached;
const article = await getArticle(id);
const html = await renderHtml(article);
await pageCache.set(cacheKey, html, { ttl: 60, tags: [`article:${id}`] });
return html;
}
真实项目还要处理并发回源,避免缓存刚失效时大量请求同时打到 SSR 服务。
五、防止缓存击穿
热点页面失效时要加锁或请求合并。
可以让一个请求负责回源,其他请求等待或返回 stale 内容。
对后端接口也要设置超时和降级。
缓存系统异常时,SSR 服务不能无限等待。
六、个性化内容要谨慎
用户昵称、购物车数量、会员价格不适合进入公共 HTML 缓存。
可以把公共内容 SSR 缓存,个性化区域客户端再请求。
也可以按登录态拆缓存,但要确保 Cookie 和缓存策略正确。
敏感页面通常不缓存 HTML,只缓存底层公共数据。
七、误区和追问
- 误区:SSR 缓存只要设置 TTL 就够。 TTL 只能自然过期,无法解决强实时更新。
- 误区:页面 URL 相同就能共用缓存。 语言、地区、登录态、实验分组都可能改变 HTML。
- 误区:缓存失效就是删除 CDN。 SSR 进程、接口层和后端也可能有缓存。
- 追问:如何避免缓存刚失效时流量打爆服务? 用请求合并、互斥锁、预热、stale-while-revalidate。
- 追问:价格页能不能缓存? 可以缓存公共结构,但价格和库存要看实时性要求,常需短 TTL 或接口实时获取。
- 追问:如何排查旧内容来自哪里? 在响应头或日志里标记缓存层级、命中状态、版本和生成时间。
八、面试收束
SSR 缓存失效的好答案要能体现系统观。
你需要说明缓存层级、key 维度、失效方式、击穿保护和个性化隔离。