CDN 与边缘缓存架构:缓存策略、击穿雪崩穿透、Cache-Control 实战

puml.online

CDN 不只是「让图片加载快」——它是边缘计算 + 缓存策略 + 安全防护的综合体。这篇是 CDN 层级架构、缓存策略、Cache-Control 头详解、缓存击穿/雪崩/穿透三种灾难及解法、Cloudflare/CloudFront/Vercel 选型。

CDN 边缘缓存层级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@startuml
skinparam componentStyle rectangle
skinparam defaultTextAlignment center

title "CDN Edge Cache Hierarchy"

actor "User" as User

rectangle "用户端" {
component "Browser Cache" as BC
}

rectangle "边缘节点" {
component "Edge PoP\n(Cloudflare/CloudFront)" as Edge
}

rectangle "区域中心" {
component "Regional Cache\n(中间层)" as Regional
}

rectangle "源站" {
component "Origin Shield" as Shield
component "Origin Server" as Origin
}

User -> BC : ① 第一次请求
BC --> User : miss

User -> Edge : ② 第二次请求
Edge --> User : hit / miss
Edge -> Regional : ③ miss
Regional --> Edge : ④ hit / miss
Regional -> Shield : ⑤ miss
Shield --> Regional : ⑥ hit / miss
Shield -> Origin : ⑦ miss
Origin --> Shield : ⑧ 响应

note right of BC
浏览器缓存
- 强缓存:Cache-Control
- 协商缓存:ETag/Last-Modified
end note

note right of Edge
边缘 PoP
- Cloudflare 200+ 节点
- CloudFront 600+ 节点
- 距离用户 0-50ms
end note

note right of Shield
Origin Shield
- 合并相同请求
- 减少回源
end note

@enduml

缓存层级意义:

  • Browser Cache — 用户本机,毫秒级
  • Edge PoP — 城市级 PoP,10-50ms
  • Regional — 大区级,50-100ms
  • Origin — 源站,100-500ms

层级越深,缓存越窄但越靠近用户

HTTP 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@startuml
title "HTTP Cache Decision Tree"

start

:收到 HTTP 响应;

if (响应头 Cache-Control: no-store)
:不缓存任何地方;
stop
elseif (Cache-Control: no-cache)
:缓存但每次 revalidate;
:服务器 ETag 比对;
if (304 Not Modified)
:用缓存;
else
:重新下载;
end
elseif (Cache-Control: max-age=N)
:缓存 N 秒;
if (N 秒内再次请求)
:直接用缓存;
else
:revalidate;
end
elseif (Cache-Control: public)
:所有中间缓存可存;
elseif (Cache-Control: private)
:只浏览器缓存;
:CDN 不能存;
elseif (Expires 过期)
:过期时间比对;
end

stop

@enduml

Cache-Control 关键值:

1
2
3
4
5
6
7
8
9
10
Cache-Control: public, max-age=31536000, immutable
# 公共缓存 1 年,永远不变(hash 文件名)
Cache-Control: public, max-age=3600
# 公共缓存 1 小时
Cache-Control: private, max-age=300
# 只浏览器缓存 5 分钟
Cache-Control: no-cache
# 缓存但每次 revalidate
Cache-Control: no-store
# 完全不缓存

s-maxagemax-age:

  • max-age — 浏览器缓存时间
  • s-maxage — CDN 缓存时间(覆盖 max-age)

ETag + Last-Modified 协商缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@startuml
title "ETag / Last-Modified Validation"

actor "User" as User
participant "Server" as Server

User -> Server : ① GET /article.html
Server --> User : ② 200 + ETag: "abc123" + Last-Modified: Mon, 01 Jan 2026

note over User : 浏览器缓存带 ETag 和 Last-Modified

User -> Server : ③ GET /article.html\nIf-None-Match: "abc123"\nIf-Modified-Since: Mon, 01 Jan 2026

alt 内容没变
Server --> User : ④ 304 Not Modified
note over User : 用浏览器缓存
else 内容变了
Server --> User : ⑤ 200 + 新内容 + 新 ETag
end

@enduml

ETag 优先生效:

  • 强 ETag — 文件内容哈希("abc123")— 字节级一致
  • 弱 ETagW/"abc123" — 语义一致(HTML 重新格式化也算相同)

缓存三种灾难

击穿(Cache Breakdown)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@startuml
title "Cache Breakdown (单 key 失效)"

participant "用户1" as U1
participant "用户2" as U2
participant "用户3" as U3
participant "Redis" as Cache
participant "DB" as DB

U1 -> Cache : ① GET hot_key
Cache --> U1 : miss (key 过期)

U2 -> Cache : ② GET hot_key
Cache --> U2 : miss (仍过期)

U3 -> Cache : ③ GET hot_key
Cache --> U3 : miss (仍过期)

note over DB
三个请求并发穿透到 DB
同一个 hot_key
QPS 突增 100 倍
end note

U1 -> DB : ④ SELECT hot_key
U2 -> DB : ④ SELECT hot_key
U3 -> DB : ④ SELECT hot_key

DB --> U1 : ⑤ slow (CPU 100%)
DB --> U2 : ⑤ slow
DB --> U3 : ⑤ slow

@enduml

修法 — 互斥锁:

1
2
3
4
5
6
7
8
9
10
def get_hot_key(key):
val = redis.get(key)
if val is None:
# 只有一个请求去 DB 加载
with redis.lock(f"lock:{key}", timeout=10):
val = redis.get(key) # double check
if val is None:
val = db.query(key)
redis.setex(key, 3600, val)
return val

修法 — 永不过期:

  • 后台异步刷新缓存
  • 业务逻辑上逻辑过期,缓存永远在

雪崩(Cache Avalanche)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title "Cache Avalanche (大量 key 同时过期)"

participant "用户" as User
participant "Redis" as Cache
participant "DB" as DB

note over Cache
2026-01-15 00:00:00
所有 key 同时过期
(设置时间相同)
end note

User -> Cache : ① GET key_1
Cache --> User : miss
User -> DB : ② SELECT key_1

User -> Cache : ③ GET key_2
Cache --> User : miss
User -> DB : ④ SELECT key_2

note over DB
100 万 key 同时过期
全部打到 DB
DB 挂掉
end note

DB --> User : ⑤ timeout

@enduml

修法:

  1. 过期时间加随机:expire = base + random(0, 300) 避免同一时间过期
  2. 多级缓存:Redis miss → 本地缓存兜底
  3. 熔断:DB 慢时熔断,直接返回降级数据

穿透(Cache Penetration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
title "Cache Penetration (查不存在的 key)"

participant "Attacker" as A
participant "Cache" as C
participant "DB" as DB

A -> C : ① GET user_id=-1
C --> A : miss (不存在)
A -> DB : ② SELECT WHERE id=-1
DB --> A : ③ null

A -> C : ④ GET user_id=-2
C --> A : miss
A -> DB : ⑤ SELECT WHERE id=-2
DB --> A : ⑥ null

note over DB
攻击者循环请求不存在的 key
绕过缓存,打 DB
DB CPU 100%
end note

@enduml

修法 — 布隆过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启动时加载所有存在的 user_id 到布隆过滤器
bloom = BloomFilter(capacity=10_000_000, error_rate=0.001)
for user in db.query("SELECT id FROM users"):
bloom.add(user.id)

def get_user(user_id):
if not bloom.contains(user_id):
return None # 一定不存在
val = redis.get(f"user:{user_id}")
if val is None:
val = db.query(user_id)
if val:
redis.setex(f"user:{user_id}", 3600, val)
return val

修法 — null 也缓存:redis.setex(f"user:{nonexistent_id}", 60, "null") — null 值也短时间缓存,防穿透攻击

CDN 缓存与 Redis 协同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@startuml
title "CDN + Redis Multi-Tier Cache"

actor "User" as User
participant "CDN Edge" as CDN
participant "Redis Cluster" as Redis
participant "Origin Server" as Origin
database "DB" as DB

User -> CDN : ① GET /api/products
CDN -> CDN : ② cache hit/miss?

alt Edge hit (10s)
CDN --> User : ③ 200 (Edge 缓存)
else Edge miss
CDN -> Redis : ④ GET product (region cache)

alt Redis hit (5min)
Redis --> CDN : ⑤ 200 (Redis 数据)
CDN --> User : ⑥ 200 + Cache-Tag: edge,redis
else Redis miss
CDN -> Origin : ⑦ GET /api/products
Origin -> DB : ⑧ SELECT
DB --> Origin : ⑨ products
Origin --> CDN : ⑩ 200 products
CDN -> CDN : ⑪ 缓存到 Edge (10s TTL)
CDN -> Redis : ⑫ SETEX products 5min
CDN --> User : ⑬ 200
end
end

@enduml

多层缓存关键:

  • Edge (10s) — 极短,挡住突发流量
  • Redis (5min) — 中等,边缘 miss 时兜底
  • Origin — 长缓存(1h+)或 no-cache

边缘函数(Edge Functions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@startuml
title "Edge Function Personalization"

actor "User" as User
participant "CDN Edge\nwith Worker" as Edge
participant "Origin" as Origin

User -> Edge : ① GET /api/recommendations
Edge -> Edge : ② Worker 读取 cookie / IP
Edge -> Edge : ③ 决定 region (北京/上海)
Edge -> Edge : ④ cache key = region + user_segment
Edge -> Edge : ⑤ 缓存命中?

alt Hit
Edge --> User : ⑥ 200 (cache)
else Miss
Edge -> Origin : ⑦ GET /recommendations?region=...
Origin --> Edge : ⑧ 200 recommendations
Edge --> User : ⑨ 200 + 缓存
end

@enduml

边缘函数能做什么:

  • A/B 测试 — 在边缘决定 variant
  • 地区路由 — CN 用户走 CN 源
  • JWT 验证 — 不回源就拒非法 token
  • Header 重写 — 注入用户 ID 等
  • 限流 — 边缘直接 429

Cloudflare/CloudFront/Vercel 对比

维度 Cloudflare CloudFront Vercel Edge
节点数 300+ 600+ 100+
边缘函数 Workers (V8) Lambda@Edge Edge Functions
价格 免费档充足 按请求 + GB 按请求 + GB
DDoS 防护 内置免费 需 WAF 套餐 基础防护
适合 全场景 AWS 生态 Next.js / Vercel

选择:

  • 个人 / 中小企业 → Cloudflare 免费档 + Workers
  • AWS 重度用户 → CloudFront + Lambda@Edge
  • Next.js 全栈 → Vercel Edge

Cloudflare Workers 实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// workers/cache-headers.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
const response = await fetch(request);
const newResponse = new Response(response.body, response);

// 添加缓存头
newResponse.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');

// 添加安全头
newResponse.headers.set('X-Content-Type-Options', 'nosniff');
newResponse.headers.set('X-Frame-Options', 'DENY');
newResponse.headers.set('Referrer-Policy', 'no-referrer');

return newResponse;
}

CloudFront Lambda@Edge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CloudFront → Lambda@Edge → Origin Response
// viewer-response.js
exports.handler = async (event) => {
const response = event.Records[0].cf.response;

// 设置缓存时间
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=86400, s-maxage=604800'
}];

// CSP 头
response.headers['content-security-policy'] = [{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'"
}];

return response;
};

实战踩坑

  • CDN 缓存了不该缓存的内容 — 用户特定页面被共享缓存。Cache-Control: private 或带 vary: Cookie。
  • 缓存过期时间太长 — 内容改了用户看不到。HTML 用 max-age=0 + ETag,JS/CSS 用 immutable + hash 文件名
  • CDN 缓存了 5xx 错误 — 源站挂了,CDN 还返 5xx。Cache-Control: no-store 或 5xx 不缓存
  • 跨域 CORS 缓存冲突 — 浏览器缓存了带 CORS 的响应,下次不带 CORS header。Vary: Origin 让 CDN 按 origin 缓存
  • 回源流量费用 — CDN miss 后回源,源站流量费CloudFront origin shield 减少回源
  • 边缘函数超时 — Lambda@Edge 5s 超时。慢逻辑不要在边缘
  • 边缘函数无状态 — 不能依赖本地内存。用 KV / Durable Objects 存状态
  • 冷启动延迟 — 边缘函数首次调用慢。预热 + keep-alive
  • 缓存 key 设计错 — 不同用户拿到相同缓存。key 含 user_id 或 session_id

决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
要缓存什么?
├─ 静态资源(JS/CSS/图片) → CDN long TTL + hash 文件名
├─ HTML → CDN short TTL + ETag
├─ API GET → Redis + CDN 兜底
├─ API POST/PUT → 不缓存
└─ 用户个性化 → Cache-Control: private,不走 CDN

CDN 选型?
├─ 个人 / 中小企业 → Cloudflare 免费
├─ AWS 生态 → CloudFront
├─ Vercel 部署 → Vercel Edge
└─ 自托管 → 自建 varnish/nginx proxy_cache

缓存策略?
├─ 完全静态 → immutable, 1 year
├─ 半静态(文章) → max-age=300, revalidate
├─ 动态 API → max-age=0, always revalidate
└─ 用户专属 → private, no CDN

最小起步:Cloudflare 免费档 + Cache-Control: public, max-age=3600进阶加边缘函数做 A/B 路由、地区路由、JWT 验证。

记住:CDN 是性能优化,不是银弹业务逻辑不要依赖 CDN 缓存(缓存可能失效),关键数据走强一致性数据库缓存策略要监控命中率——命中率 < 80% 说明策略有问题。

  • 标题: CDN 与边缘缓存架构:缓存策略、击穿雪崩穿透、Cache-Control 实战
  • 作者: puml.online
  • 创建于 : 2026-07-30 18:05:00
  • 更新于 : 2026-08-14 21:34:29
  • 链接: https://puml.online/blog/plantuml-cdn-edge-cache/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。