@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
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
defget_hot_key(key): val = redis.get(key) if val isNone: # 只有一个请求去 DB 加载 with redis.lock(f"lock:{key}", timeout=10): val = redis.get(key) # double check if val isNone: val = db.query(key) redis.setex(key, 3600, val) return val
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)
defget_user(user_id): ifnot bloom.contains(user_id): returnNone# 一定不存在 val = redis.get(f"user:{user_id}") if val isNone: val = db.query(user_id) if val: redis.setex(f"user:{user_id}", 3600, val) return val
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