写点什么

艾体宝干货 | 三层缓存架构扛住流量冲击,源站稳如泰山

作者:艾体宝IT
  • 2025-12-26
    江苏
  • 本文字数:3481 字

    阅读完需:约 11 分钟

在后端开发中,Redis 几乎就是缓存的代名词。用它撑业务规模的扩张确实信手拈来,但每逢大促、新品上线这类关键节点,总不乏意外发生:缓存命中率跳水,源站 CPU 飙升到 100%,值班群里的告警提示刷个不停。而这时再堆 Redis 节点往往是杯水车薪,真正的解法,是搭建一套「CDN + 边缘 KV + 源站缓存」的分层架构,配上灵活的 TTL 策略,让缓存命中成为常态,源站彻底告别击穿焦虑。

缓存的分工逻辑

缓存的核心是 “让数据离用户更近”,分层架构就是把这个逻辑做到极致。我把它总结为 “三环防御体系”,每一层都有明确的职责,且共用一套核心规则:

第一层:CDN 全球前置

作为用户请求的 “第一接触点”,CDN 最擅长处理\\准静态内容\\—— 比如带签名的图片、公开 API 响应、静态 HTML 片段,只要能通过 “URL + 请求头” 定位的内容,都该交给它。优势是地理节点密集,用户在哪都能快速取到数据,直接挡住 70% 以上的源站请求。

第二层:边缘 KV 承接动态热点

CDN 搞不定完全动态的内容(比如个性化推荐片段、实时聚合的 API 结果),但这些内容重新计算又特别费资源。这时边缘 KV 就派上用场了 —— 像 Cloudflare KV、Vercel Edge Store 这类服务,能在每个区域提供低延迟的键值存储,专门缓存 “热点动态数据”。比如电商商品详情页的库存模块、用户的个性化首页片段,用它存一波,响应速度能快上几十毫秒。

第三层:Redis/Memcached 兜底

这层是离源站最近的最后一道防线,存那些不适合放边缘但又天天被访问的东西,比如用户登录后的会话信息、数据库查出来的订单统计。

这三层要共用一套语言体系:​软 TTL、硬 TTL、失效重验证、击穿防护​,这样缓存逻辑就不会乱。

生产级 TTL 策略:如何既保速度又保准确

很多人用缓存只设一个过期时间,这是最大的误区。真正能抗住压力的缓存,靠的是分层过期逻辑:

核心概念

  • 硬 TTL​:绝对过期时间,过了这个点,数据再用就可能出问题,必须作废。比如商品价格缓存,硬 TTL 设 10 分钟,需要保证不会展示过期售价。

  • 软 TTL​:“新鲜度窗口”,过了这个时间,数据不算 “最新”,但还能用。比如商品详情页的描述信息,软 TTL 设 1 分钟,轻微延迟更新不影响用户体验。

  • Stale-while-revalidate(SWR)​:过了软 TTL 后,先给用户返回过期数据,同时偷偷去源站刷新缓存。用户感觉不到延迟,数据也能慢慢更。

  • Stale-if-error​:源站挂了的最后一层保障,这时只要没超过硬 TTL,就返回过期数据。

这些策略不是空谈,CDN 能通过 HTTP 头配置,Redis、边缘 KV 能通过代码实现,上手很简单。

实操指南:从配置到代码落地

CDN 层

CDN 的缓存逻辑靠 Cache-Control 头控制,我常用这套配置:

Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=600Vary: Authorization, Accept-EncodingSurrogate-Key: product:123 category:shoes
复制代码

之前我们帮助客户优化首页 API 时,加了这几行头,源站请求量直接降了 80%,p95 延迟从 500ms 压到 150ms。

边缘 KV 层:动态内容的缓冲

以 Cloudflare Workers 为例,用边缘 KV 实现分层 TTL 其实很简单。核心逻辑是 先查缓存,新鲜就返回,软过期就兜底 + 刷新,硬过期再回源:

// edge-cache.ts (Cloudflare/Vercel 风格边缘运行时)type CacheEntry = { body: string; softExp: number; hardExp: number };
const SOFT_TTL = 60; // 秒const HARD_TTL = 600;
export default async function handler(req: Request) { const key = await cacheKey(req); // 例如:稳定的 URL + 用户细分 const now = Math.floor(Date.now() / 1000);
// 1) 尝试边缘 KV const kvHit = await EDGE_KV.get<CacheEntry>(key, "json"); if (kvHit) { if (now <= kvHit.softExp) return ok(kvHit.body, true); // 新鲜数据 // 软过期:返回过期数据 + 在后台刷新 refreshLater(key, req); if (now <= kvHit.hardExp) return ok(kvHit.body, true); // 允许返回过期数据 }
// 2) 回退到源站(带缓存击穿控制) const body = await singleFlight(key, () => fetchOrigin(req).then(r => r.text()));
// 3) 直写 KV,并设置软/硬 TTL await EDGE_KV.put(key, JSON.stringify({ body, softExp: now + SOFT_TTL, hardExp: now + HARD_TTL }), { expiration: now + HARD_TTL });
return ok(body, false);}
function ok(body: string, cached: boolean) { return new Response(body, { headers: { "Content-Type": "application/json", "X-Edge-Cache": cached ? "HIT" : "MISS" } });}
// 单飞模式防止热点键上的惊群效应const inflight = new Map<string, Promise<string>>();async function singleFlight(key: string, fn: () => Promise<string>) { if (inflight.has(key)) return inflight.get(key)!; const p = fn().finally(() => inflight.delete(key)); inflight.set(key, p); return p;}
// 发射后不管的刷新function refreshLater(key: string, req: Request) { // 特定平台的 waitUntil/queue;建议最小退避 (globalThis as any).waitUntil?.(singleFlight(key, () => fetchOrigin(req).then(r => r.text()) .then(body => EDGE_KV.put(key, JSON.stringify({ body, softExp: Math.floor(Date.now()/1000) + SOFT_TTL, hardExp: Math.floor(Date.now()/1000) + HARD_TTL }), { expirationTtl: HARD_TTL })));}
复制代码

这里的 single-flight 特别重要,热点键过期时,不会几百个请求同时回源,而是只让一个请求去拉数据,其他人等着拿结果,源站瞬间压力大减。

Redis 层:加抖动避免同步过期

Redis 里实现软 / 硬 TTL 也类似,但要多一步 TTL 抖动,比如本来硬 TTL 设 10 分钟,实际存的时候随机加 / 减 1-2 分钟。不然大量缓存同时过期,还是会引发 “雪崩”。

给个 Python 示例:

import json, random, timeimport redis
r = redis.Redis()SOFT, HARD = 60, 600
def get_cache(key: str, loader): now = int(time.time()) raw = r.get(key) if raw: entry = json.loads(raw) if now <= entry["soft"]: # 新鲜数据 return entry["val"], True # 近过期概率性刷新 if random.random() < 0.1: _refresh_async(key, loader) if now <= entry["hard"]: return entry["val"], True # 允许返回过期数据 # 未命中或硬过期 val = loader() soft = now + SOFT + random.randint(-5, 5) hard = now + HARD + random.randint(-30, 30) r.setex(key, HARD, json.dumps({"val": val, "soft": soft, "hard": hard})) return val, False
复制代码

避坑指南

分层缓存看着复杂,但只要避开几个坑,基本不会出问题。

别按 “用户 ID” 做缓存键

之前有个客户给个性化首页做缓存,键用的是 “user:123:home”,结果每个用户都是 miss,缓存等于白加,还浪费了一堆存储。后来改成按 “用户分段”—— 比如 “会员:华东区:home”,把同套餐同区域的用户归为一类,命中率直接从 5% 飙到 80%。

缓存头千万别漏

CDN 没 Cache-Control 头,它就会直接回源。新手可以先无脑加 max-age=60, stale-while-revalidate=600,再慢慢调参数。

别把边缘 KV 当数据库

边缘 KV 是缓存不是存储,写操作一定要落源站,再通过刷新机制同步到边缘。之前我们的客户试过在边缘直接写数据,结果不同区域数据不一致,排查了半天。

清除缓存别一刀切

全量清缓存等于给源站开闸,瞬间请求量能翻 10 倍。要用 Surrogate-Key 按标签清,比如改了某款商品,只清 product:123 的缓存,影响范围最小。

可观测性

缓存搭好不是结束,得知道它有没有用。在 Grafana 里加这 5 个面板,出问题能秒定位。

  1. 各层命中率(CDN、边缘 KV、Redis):低于 70% 就得调策略;

  2. 过期内容使用率:太高说明源站压力大,得加软 TTL;

  3. single-flight 并发数:突然飙升可能是热点键来了;

  4. 各区域延迟(p50/p95):看缓存对用户体验的实际影响;

  5. 错误导致的过期内容占比:太高说明源站不稳定。

总结

单一 Redis 能解决很多问题,但要撑住高并发、全球访问、动态内容,依赖分层架构 + 灵活策略的体系更为稳妥。CDN 扛静态,边缘 KV 扛动态,Redis 兜底,再配上软 / 硬 TTL、single-flight、抖动这些细节,真正做到源站稳了,用户快了,值班也能睡个安稳觉了。

如果刚开始试,不用一步到位,我们建议先搞 CDN + Redis 组合,跑顺了再加边缘层,成本低还见效快。

用户头像

艾体宝IT

关注

还未添加个人签名 2024-10-11 加入

还未添加个人简介

评论

发布
暂无评论
艾体宝干货 | 三层缓存架构扛住流量冲击,源站稳如泰山_redis_艾体宝IT_InfoQ写作社区