Chapter 04

缓存体系设计

速度的秘密武器。从 CDN 到数据库缓存,理解四种缓存策略,
以及击穿、穿透、雪崩三大经典问题的系统性解法。

缓存分层体系

现代系统的缓存不是单一的,而是层次化的。每一层都在最接近用户的位置拦截请求,减少下一层的压力。

缓存层次从用户到数据库: 用户浏览器 ┌─────────────────────────────────────────────────────┐ │ Browser Cache(浏览器缓存) │ │ Cache-Control: max-age=3600 │ │ 存:图片、CSS、JS、不频繁更新的 API 响应 │ └──────────────────────────┬──────────────────────────┘ │ Cache Miss ▼ ┌─────────────────────────────────────────────────────┐ │ CDN(内容分发网络) │ │ Cloudflare / AWS CloudFront / Akamai │ │ 全球边缘节点缓存静态资源和可缓存的 API 响应 │ │ 命中率目标:> 90%(静态资源),30-60%(动态 API) │ └──────────────────────────┬──────────────────────────┘ │ Cache Miss ▼ ┌─────────────────────────────────────────────────────┐ │ 应用层缓存(Redis / Memcached) │ │ 热点数据:用户 Profile、Feed、商品详情、计数器 │ │ 命中率目标:> 95% │ └──────────────────────────┬──────────────────────────┘ │ Cache Miss ▼ ┌─────────────────────────────────────────────────────┐ │ 数据库查询缓存 / Buffer Pool │ │ MySQL InnoDB Buffer Pool、PostgreSQL Shared Buffers │ │ 热页面数据常驻内存,减少磁盘 I/O │ └──────────────────────────┬──────────────────────────┘ │ Cache Miss ▼ 磁盘存储(最慢,最贵)

四种缓存读写策略

策略一:Cache-Aside(旁路缓存)

最常见的策略。应用程序自己负责管理缓存,缓存在应用侧,数据库在旁路。

读取流程: Application Cache (Redis) Database │ │ │ │──GET user:123──────▶│ │ │ │ │ ┌──┤ Cache Hit? │ │ │ │◀──返回数据──────────│ │ │ │ 命中✓ │ │ │ │ │ │ └──┤ Cache Miss │ │ │──────────────────────────────SELECT──────▶│ │◀──────────────────────────────返回数据─────│ │──SET user:123, TTL=3600────────────────▶ │ │◀──OK────────────────────────────────── │ │ │ │ 写入流程(先写DB,再删缓存): Application Cache (Redis) Database │ │ │ │──────────────────────────────UPDATE──────▶│ │◀──────────────────────────────OK──────────│ │──DEL user:123──────▶│ │ │◀──OK────────────────│ │ 下次读取时从 DB 重新加载并回填缓存
⚠ 为什么写时删缓存而不是更新缓存?

如果先更新缓存再写 DB,并发场景下两个写操作可能导致缓存和 DB 数据不一致。删除比更新安全:删除是幂等的,最坏情况是下次读时 Cache Miss,从 DB 重新加载,不会产生脏数据。

策略二:Read-Through(读穿透)

Read-Through 缓存: Application Cache Provider Database │ │ │ │──GET user:123──────▶│ │ │ │ (Miss) 自动查DB─────▶│ │ │◀────────────────────│ │◀──返回数据──────────│ (自动缓存) │ │ │ │ 区别:Cache Miss 时,由缓存层自动去 DB 加载,应用无需处理 代表产品:AWS ElastiCache(某些模式)、Ehcache(只读缓存模式)

策略三:Write-Through(写穿透)

Write-Through 缓存: Application Cache Provider Database │ │ │ │──WRITE user:123────▶│ │ │ │──同步写 DB──────────▶│ │ │◀──OK────────────────│ │◀──OK────────────────│ │ 写缓存 = 写 DB(同步双写) 优点:缓存与DB强一致,读取永远是最新数据 缺点:写延迟高(要等 DB 返回),写入频繁时缓存浪费(很多数据写后不被读)

策略四:Write-Behind / Write-Back(异步回写)

Write-Behind 缓存: Application Cache Provider Database │ │ │ │──WRITE user:123────▶│ │ │◀──OK(立即返回)────│ │ │ │ 异步批量写 ·····▶ │ │ │ (几秒后) │ 写缓存立即返回,异步批量刷写 DB 优点:写性能极高(内存速度),合并多次写减少 DB 压力 缺点:缓存宕机可能丢数据(DB 还未同步) 适用:计数器(点赞数、浏览量)、日志写入、非关键数据
策略一致性读性能写性能适用场景
Cache-Aside最终一致正常通用,最常用
Read-Through最终一致正常读密集,缓存层封装
Write-Through强一致写少读多,数据敏感
Write-Behind最终一致极高高频写,可容忍少量丢失

三大经典缓存问题

问题一:缓存击穿(Cache Breakdown)

一个热点 key 突然过期,大量并发请求同时打到数据库。

缓存击穿场景: 时刻 T:Redis 中「双11首页商品」的缓存 Key 过期 同时有 10,000 个请求: Request 1 ──▶ Redis MISS ──▶ DB 查询 ... Request 2 ──▶ Redis MISS ──▶ DB 查询 ... Request 3 ──▶ Redis MISS ──▶ DB 查询 ... ... Request 10000 ──▶ Redis MISS ──▶ DB 查询 ... 💥 DB 过载! 解决方案: 方案A:互斥锁(Mutex Lock) 只有第一个 Cache Miss 的请求能拿到锁去查 DB 其他请求等待或返回降级数据 伪代码: value = redis.get(key) if value == null: if redis.setnx(lock_key, 1, TTL=5s): // 只有一个拿到锁 value = db.query(...) redis.set(key, value, TTL=3600) redis.del(lock_key) else: sleep(50ms) return redis.get(key) // 等其他线程回填 方案B:逻辑过期(永不物理过期) 在缓存值中存入「逻辑过期时间」 过期时异步刷新,期间继续返回旧值 彻底避免 DB 被打,但读到的可能是稍旧的数据

问题二:缓存穿透(Cache Penetration)

查询根本不存在的数据,每次都打穿缓存直接到 DB。常见于恶意攻击(传入非法 ID)。

缓存穿透场景: 攻击者发送:GET /user?id=-1, id=-2, id=-999999... 每个 ID 都不存在 → Redis MISS → DB 查询 → 返回 null → 不写缓存(因为没数据)→ 下次同样 ID 继续打 DB 解决方案: 方案A:缓存空值 DB 返回 null,也在 Redis 中存入空值 redis.set("user:-1", "null", TTL=60s) 下次查询直接返回缓存的 null,不打 DB 注意:TTL 不要太长,否则若后来数据被创建会读到旧的 null 方案B:布隆过滤器(Bloom Filter)✓ 推荐大型系统 系统启动时将所有合法 ID 加入布隆过滤器 请求到来先查布隆过滤器: → 不存在(100%确定)→ 直接返回 404,不查 Redis/DB → 可能存在 → 再去 Redis/DB 查 布隆过滤器特性: - 空间极省:1亿个元素只需约 125MB - 查询极快:O(k) 哈希计算 - 有误判率(假阳性),但不会漏判(假阴性)

问题三:缓存雪崩(Cache Avalanche)

大量缓存 key 在同一时间集中过期,或 Redis 集群宕机,导致全部流量涌入 DB。

缓存雪崩场景: 凌晨2点批量缓存刷新,所有商品缓存 TTL=3600s(同时设置) → 一小时后,凌晨3点,所有缓存同时过期 → 突发大量 DB 请求 💥 解决方案: 1. TTL 随机化(最简单!) TTL = base_ttl + random(0, 600) // 加随机抖动 将集中过期分散到 60 分钟窗口内 2. 高可用 Redis 集群 Redis Sentinel(主从哨兵) Redis Cluster(分片集群) 避免 Redis 单点宕机造成全量 Cache Miss 3. 多级缓存(本地 + 分布式) 本地内存缓存(Caffeine/Guava)作为第一层 Redis 作为第二层 即使 Redis 雪崩,本地缓存仍能抗住部分流量 4. 限流 + 熔断(最终防线) 即便缓存全部失效,DB 层也有限流保护 超过 DB 承载上限时,返回降级响应
▶ 面试要点:三者区分

CDN 缓存策略

CDN(内容分发网络)是最外层的缓存,将内容推送到距离用户最近的边缘节点。

CDN 工作原理(以 Cloudflare 为例): 用户(东京) CDN 边缘(东京) 源站(美国) │ │ │ │─GET /images/logo.png───────▶│ │ │ │ 缓存 HIT │ │◀────────────────────────────│ 延迟 <5ms │ │ │ │ │─GET /api/products──────────▶│ │ │ │ 缓存 MISS │ │ │──────────────────────▶│ │ │◀─────────────────────│ │◀──返回(约150ms RTT)────────│ │ CDN 缓存控制头: Cache-Control: public, max-age=86400 // CDN 可以缓存,1天 Cache-Control: private, no-cache // 只有浏览器缓存,不让 CDN 缓存 Cache-Control: s-maxage=3600 // 专门针对 CDN 的缓存时间 Vary: Accept-Encoding, Accept-Language // 按请求头区分缓存版本
💡 Twitter/Netflix 的缓存实践

Twitter 的 Memcached 集群缓存了数十亿条推文,命中率超过 99%。Netflix 使用 EVCache(基于 Memcached)在多地区复制缓存数据,每秒处理数百万次缓存读取。他们的原则:让绝大多数读请求永远不到达数据库