· 王思腾 · 系统架构 · 8 分钟阅读

缓存设计实战:穿透、击穿、雪崩与一致性

缓存是高并发系统的第一道防线,但用不好会引入数据不一致、缓存穿透、击穿、雪崩等问题。本文系统梳理缓存读写策略(Cache Aside/Read/Write Through)、三大经典问题的成因与解法、热点 key、缓存与数据库一致性,以及 Redis 在生产中的实践要点。

缓存是高并发系统的第一道防线,但用不好会引入数据不一致、缓存穿透、击穿、雪崩等问题。本文系统梳理缓存读写策略(Cache Aside/Read/Write Through)、三大经典问题的成因与解法、热点 key、缓存与数据库一致性,以及 Redis 在生产中的实践要点。

“性能不够,缓存来凑”——缓存是提升读性能、抵抗高并发最直接的手段:把热点数据放在内存(Redis 等),避免每次都打到数据库。但缓存绝非“加个 Redis”那么简单,用不好会带来数据不一致和一连串经典故障。

一、缓存放在哪、缓存什么

多级缓存是常见做法:

flowchart LR
    C1["浏览器 / CDN 缓存"] --> C2["本地缓存<br/>Caffeine / 进程内"] --> C3["分布式缓存<br/>Redis"] --> DB[("数据库")]
  • 本地缓存:最快但不共享、容量小,适合变更极少的配置/字典数据;
  • 分布式缓存(Redis):多实例共享、容量大,是主力;
  • 缓存对象:读多写少、可容忍短暂不一致的热点数据(商品详情、用户信息、配置、排行榜)。

二、缓存读写策略

最常用的是 Cache Aside(旁路缓存):

  • 读:先读缓存,命中就返回;未命中则读数据库,写入缓存后返回;
  • 写:先更新数据库,再删除缓存(而不是更新缓存)。
flowchart TB
    subgraph Read["读流程(Cache Aside)"]
        R0["读请求"] --> R1{"缓存命中?"}
        R1 -- 是 --> R2["直接返回"]
        R1 -- 否 --> R3["读数据库"] --> R4["回填缓存"] --> R2
    end
    subgraph Write["写流程"]
        W1["更新数据库"] --> W2["删除缓存<br/>下次读时回填最新值"]
    end

为什么写操作是“删缓存”而不是“更新缓存”?

  • 避免并发写导致缓存与库不一致;
  • 很多缓存值是复杂计算结果,删了等下次读时懒加载更省事;
  • 防止写多读少时白做无用功。

其他策略(Read Through / Write Through / Write Behind)把缓存和库的同步封装在缓存层,业务侧较少直接用,理解旁路模式即可应对大多数场景。

三、三大经典问题

1. 缓存穿透(Penetration)

查一个数据库里根本不存在的数据,缓存永远不命中,请求全打到数据库。恶意攻击时尤其危险。

解法:

  • 缓存空值:查不到也缓存一个空结果(设较短 TTL);
  • 布隆过滤器(Bloom Filter):在缓存前加一层,快速判断“这个 key 一定不存在”,直接拦截;
  • 参数校验,非法请求提前拒绝。

2. 缓存击穿(Breakdown)

某个热点 key 恰好过期的瞬间,大量并发请求同时涌入,都发现缓存没命中,一起打到数据库重建缓存。

解法:

  • 互斥锁 / 单飞(singleflight):只让一个线程去查库重建缓存,其他线程等待或返回旧值;
  • 逻辑过期:缓存不设物理 TTL,值里带逻辑过期时间,发现过期后异步更新,期间返回旧值;
  • 热点 key 永不过期 + 后台异步刷新。

3. 缓存雪崩(Avalanche)

大量 key 在同一时刻集中过期,或缓存节点整体宕机,导致请求洪流全部压向数据库。

解法:

  • 过期时间加随机抖动(如 TTL = 基础值 + random),避免集中失效;
  • 缓存高可用:Redis 哨兵/集群,避免单点;
  • 限流降级 + 兜底:数据库扛不住时限流,保护核心链路;
  • 关键数据提前预热,别等流量来了才加载。

四、缓存与数据库一致性

这是缓存最容易出问题的地方。Cache Aside 的“先更新库、再删缓存”在极端并发下仍可能不一致:

  • 典型场景:读线程读库(旧值)→ 写线程更新库并删缓存 → 读线程把刚才读到的旧值写回缓存 → 缓存长期脏数据。

工程上的应对:

  • 删除缓存失败要重试(用消息队列/订阅 binlog 异步补偿);
  • 设置过期时间兜底:即使出现不一致,TTL 后也能自愈;
  • 强一致要求高的数据,干脆不缓存或加锁;
  • 进阶:通过订阅数据库 binlog(如 Canal) 来驱动缓存失效,把一致性收敛到一条可靠链路。

记住:缓存本质是用“短暂的不一致”换性能,要评估业务能否容忍、能容忍多久。

五、热点 key 与大 key

  • 热点 key:单个 key 访问量极高(如秒杀商品),可能打满单个 Redis 节点网卡。处理:本地缓存兜底、热点 key 拆分(多副本分摊)、读多副本。
  • 大 key:一个 value 过大(几 MB 的 hash/list),导致单次操作阻塞、迁移卡顿。处理:拆分成小 key、分页存取、避免存大对象。

六、Redis 生产实践要点

  • 数据结构选对:String、Hash、ZSet(排行榜/延迟队列)、Set(去重)、List,别什么都用 String 存 JSON;
  • 设置合理 TTL:几乎所有 key 都应有过期时间,防止冷数据常驻;
  • 避免热 key/大 key,避免用 KEYS *、FLUSHALL 等阻塞命令,用 SCAN;
  • 持久化与高可用:RDB/AOF、哨兵或 Cluster;
  • 内存与淘汰策略:设 maxmemory 和合适的淘汰策略(如 allkeys-lru);
  • 缓存不是数据库:别把唯一真实数据只放缓存。

七、一张速查表

问题现象主流解法
穿透查不存在的数据缓存空值、布隆过滤器、参数校验
击穿热点 key 过期瞬间被打爆互斥锁重建、逻辑过期、热点不过期
雪崩大量 key 同时失效/缓存宕机TTL 加随机、高可用、限流降级、预热
不一致缓存与 DB 数据不符先更库再删缓存、删除重试、TTL 兜底、binlog
热点/大 key单点过热/操作阻塞本地缓存、key 拆分、避免大对象

八、小结

  1. 主力模式是 Cache Aside:读未命中回源并回填,写时先更新库再删缓存;
  2. 三大故障要分清——穿透(查不存在的)、击穿(热点过期)、雪崩(集中失效/宕机),解法各不同;
  3. 一致性靠“删缓存 + 失败重试 + TTL 兜底”,强一致场景慎用缓存;
  4. 关注热点 key、大 key、TTL 和高可用,把缓存当作可失效的加速层而非数据源。

缓存设计的本质,是在性能、成本和一致性之间做清醒的权衡——先想清楚数据能容忍多大的不一致,再决定怎么缓存。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
从 0 到 100 万用户:架构演进与系统扩展性设计

从 0 到 100 万用户:架构演进与系统扩展性设计

没有一步到位的架构,只有随规模不断演化的架构。本文按用户量级(单机 → 垂直扩展 → 水平扩展 → 服务化 → 多机房)梳理系统在不同阶段遇到的瓶颈与应对,讲清扩展性的两个方向(垂直/水平)、无状态化、分库分表、缓存与读写分离,以及“什么时候该做什么”的取舍。

电商系统架构设计:从下单到履约的全链路

电商系统架构设计:从下单到履约的全链路

电商系统的复杂性藏在库存、订单、支付与履约的每一次协同里。本文从业务域划分出发,带你走一遍下单主链路,并重点讨论超卖、幂等、分布式事务与大促高并发的应对方案。

分布式锁原理与实践:从 Redis 锁到 Redlock

分布式锁原理与实践:从 Redis 锁到 Redlock

单机锁管不住跨进程、跨节点的并发。分布式锁用于在多实例间互斥访问共享资源(防超卖、防重复任务、幂等)。本文先讲清分布式锁的四大要求(互斥、防死锁、可重入、解铃还须系铃人),再逐个聊 SETNX、Lua 原子性、看门狗续期、Redlock 争议,以及基于数据库/ZooKeeper 的方案与选型。