一、引言
CDN 的本质是「把热数据推到离用户最近的地方」,但真正决定命中的,是源站(Origin)与缓存层之间如何配合。很多团队只配置了边缘 TTL,却忽略了源站的回源链路、穿透防护和动态内容怎么缓存——结果边缘命中率虚高,一遇热点源站直接被打穿。
本文拆解源站与缓存策略的核心问题:回源链路怎么优化、缓存穿透/击穿/雪崩怎么防、Origin Shield 分层怎么搭、动态内容怎么变通缓存,最后给出一份「静态 + API」差异化的缓存策略表。基础概念见 边缘缓存策略。
二、回源链路优化:让「没命中」也快
2.1 回源路径
用户 → 边缘 PoP →(未命中)→ 源站
回源延迟 = 边缘到源站的网络 + 源站处理
边缘离源站越远,回源越慢
2.2 回源优化三板斧
1. 回源就近:边缘回源走「最近的源站」(多源站 / 按地域回源)
2. 回源压缩:源站开 gzip/br,回源体积更小
3. 回源协议:HTTP/2 到源站,减少连接开销
| 优化 | 收益 |
|---|---|
| 就近回源 | 回源 RTT 从跨洲降到区域级 |
| 压缩 | 回源带宽与延迟双降 |
| 连接复用 | 省握手 |
心法:边缘命中率高≠快,回源快的系统才是真的快。冷内容、首次访问、缓存刷新都会打到源站,回源优化决定这部分的体验。
三、缓存穿透、击穿、雪崩
3.1 三类缓存故障
穿透:请求的数据「本来就不存在」(如查不存在的 id)→ 永远不命中 → 每次都打源站
击穿:某个「热点 key」过期瞬间 → 大量请求同时打源站
雪崩:大量 key「同时过期」→ 源站瞬间被打爆
3.2 穿透防护
缓存穿透:把「不存在」也缓存起来
- 空值缓存:存一个「空标记」,TTL 短(如 5min)
- 布隆过滤器:请求前先判断「大概率不存在」→ 直接返回
// 空值缓存:查不到也写缓存
const v = await cache.get(`order:${id}`)
if (v === undefined) {
const data = await db.query(id)
await cache.set(`order:${id}`, data ?? EMPTY, data ? 3600 : 300)
}
3.3 击穿防护
热点 key 过期 → 单飞(mutext):
只有一个请求去回源重建缓存,其余等待
// 单飞重建:防止热点 key 过期瞬间打爆源站
const lock = await cache.setnx(`lock:order:${id}`, '1', { ttl: 10 })
if (lock) {
const data = await db.query(id)
await cache.set(`order:${id}`, data, 3600)
await cache.delete(`lock:order:${id}`)
} else {
await sleep(50); return cache.get(`order:${id}`) // 等重建
}
3.4 雪崩防护
大规模同时过期 → 打爆源站
对策:过期时间加随机抖动(3600 + rand(0,300))
错峰过期,避免「齐刷刷失效」
心法:三类故障本质都是「缓存失效瞬间的流量洪峰」。穿透靠空值缓存、击穿靠单飞、雪崩靠错峰——三招都是从「把源站当盾」变成「把缓存当盾」。
四、Origin Shield:给源站再加一道缓冲
4.1 什么是 Origin Shield
边缘 PoP(数百个)──→ Origin Shield(区域聚合)──→ 源站
└── 所有边缘回源先打 Shield,Shield 再回源
好处:源站只被 Shield 打,回源次数从「PoP 数 × 命中失败」降为「Shield 数」
4.2 两层缓存
边缘缓存(离用户近,TTL 短,命中率靠用户流量)
Shield 缓存(离源站近,TTL 长,命中率靠聚合回源)
└── 冷内容:边缘 miss → Shield 命中 → 源站几乎不被打
| 缓存层 | 位置 | 作用 | TTL |
|---|---|---|---|
| 边缘 | 各地 PoP | 就近命中 | 短 |
| Shield | 区域中心 | 聚合回源 | 长 |
心法:Shield 是「缓存里的缓存」。它不直接服务用户,而是服务边缘——把「打源站」变成「打 Shield」,源站才能从「被打穿」变成「几乎不被直接打」。
五、动态内容缓存:边缘 404 / 零 TTL / 分区缓存
5.1 动态内容能不能缓存
完全动态(个性化 feed、实时状态)→ 缓存会出错
但很多「动态」其实有变通空间
5.2 三种变通
| 变通 | 做法 | 适用 |
|---|---|---|
| 边缘 404 | 命中直接回 404,不把请求打到源站 | 确定性不存在的路径 |
| 零 TTL 但有缓存 | Cache-Control: max-age=0, s-maxage=60 | 内容准实时,秒级刷新 |
| 分区缓存 | 按用户群/地域分区缓存 | 半个性化(按区不按人) |
动态接口的缓存分层:
├── 全局共有部分(配置、版本)→ 长缓存
├── 分区部分(地域/语言)→ 分区缓存
└── 个人部分(我的数据)→ 不缓存 / 短 TTL
5.3 动态内容的 Cache-Control
// 动态但可复用:让 CDN 缓存 60s,浏览器不缓存
Cache-Control: public, max-age=0, s-maxage=60
心法:动态内容不是「不能缓存」,而是「分层缓存」。把响应拆成「共有 + 分区 + 个人」三层,共有层和分区层都能用 CDN,只有个人层走源站。
六、静态资源与 API 的差异化策略
6.1 静态资源
静态资源(js/css/img):
- 文件名带 hash → 长期缓存(一年)
- Cache-Control: public, max-age=31536000, immutable
- 版本变化靠「文件名变化」而非「过期」
6.2 API
API 响应:
- 不变化的(配置/字典)→ 长缓存
- 准实时 → s-maxage 短缓存
- 强动态 → 不缓存 + 回源优化
| 内容 | TTL | 策略 |
|---|---|---|
| 静态资源 | 1 年 | immutable |
| 配置/字典 | 1 天 | s-maxage |
| 列表数据 | 60s | s-maxage |
| 个性化接口 | 0 | 回源优化 |
铁律:静态靠长缓存,动态靠回源优化。对「不能缓存的内容」,把力气花在回源链路(就近、压缩、并发)而不是硬缓存。
七、缓存刷新与一致性
7.1 主动刷新
内容更新后需要「让旧缓存失效」:
- 刷新单 URL(CDN 提供 purge API)
- 批量刷新(目录 / 前缀)
- 服务端标记版本 + 客户端带版本请求
7.2 刷新风暴
全局刷新瞬间 → 所有边缘同时回源 → 源站被打爆
对策:分批刷新、错峰、先预热后刷新
心法:缓存的难点不在「怎么缓存」,在「怎么让该失效的失效」。发布系统要带「预热 + 定向刷新」能力,而不是全站 purge。
八、总结
源站与缓存策略的核心是「给源站减负」:
- 回源优化:就近、压缩、连接复用,让没命中也快。
- 三类防护:穿透靠空值缓存、击穿靠单飞、雪崩靠错峰。
- Origin Shield:边缘打 Shield、Shield 打源站,源站几乎不被直接打。
- 动态分层:共有/分区/个人三层,能缓的分层缓、不能的走回源优化。
- 刷新可控:定向刷新 + 预热,别全站 purge。
把源站从「被边缘直接打的靶子」变成「被 Shield 保护的稳定服务」,配合 Webhook 集成 的主动失效与 可观测性 的命中率监控,缓存体系就能既快又不脆。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。