← Back to Blog

缓存不是加一层就完事:讲透多级缓存的三层架构

47 阅读 点赞

很多人第一次给系统加缓存,都是在数据库扛不住的时候:接口慢、QPS 上不去,于是加一层 Redis,指标立刻好看起来。可没过多久,新的问题冒出来了——缓存和数据库不一致,用户看到的是过期数据;某个热点 key 突然失效,数据库瞬间被打穿;扩容加机器,缓存命中率反而往下掉。

缓存从来不是「加一层就完了」,它是一套需要分层设计、想清楚失效边界和一致性代价的架构。这篇文章把缓存拆成三层来讲:越靠近请求的那一层越快也越贵,越靠近数据的那一层越准也越慢,真正的功夫在于让每一层各司其职、并且想清楚它们之间怎么交接。

多级缓存分层概念封面
多层磨砂玻璃立方体:越靠近请求越快,越靠近数据越准

第一层:把「快」和「贵」放在离用户最近的地方

最靠近用户的缓存层,通常跑在应用进程内部或者 CDN 边缘节点上,常见形态是本地内存缓存(如 Caffeine、Guava Cache)和 HTTP/CDN 缓存。它的特点非常鲜明:访问延迟在微秒级,没有网络往返,但容量小、无法在多个实例之间共享,而且进程一重启就全没了。

这一层的价值在于挡住那些「读得极多、变得极少」的数据。比如商品类目、字典表、配置项、权限码表。这类数据更新频率可能是天级甚至周级,但每次页面渲染都要读,走本地缓存几乎零成本。反过来说,如果一个数据每秒都在变,把它放进本地缓存就是自找麻烦——很快各实例之间就会不一致。

  • 适合:读多写少、容忍短暂不一致、单条数据小、热点集中的数据。
  • 不适合:频繁变更的状态类数据、需要强一致的金额/库存数据。
  • 关键参数:容量上限 OOM 风险、过期时间的抖动、主动失效的广播渠道。

这一层最容易被忽视的坑是实例间不同步。本地缓存没有天然的一致性机制,当一个实例收到更新后,其他实例的旧值还在内存里。常见解法是引入一个轻量的失效广播(Redis 的 Pub/Sub 或专门的失效队列),让所有实例在数据变更时同步清掉对应 key,而不是等待它自然过期。

第二层:分布式缓存,真正的共享层与流量闸门

三层同心玻璃环缓存结构示意
三层同心环:从本地缓存到分布式缓存再到数据源,逐层收敛

第二层是绝大多数系统里缓存的主力:以 Redis、Memcached 为代表的分布式缓存。它横跨所有应用实例,容量可以从几 GB 扩到几百 GB,是一道真正意义上的流量闸门——数据库能不能被保护住,主要看这一层的命中率。

这一层最核心的设计问题有两个:缓存穿透和缓存击穿。穿透指的是查询一个根本不存在的 key,缓存里没有、数据库里也没有,每次请求都直落数据库;解法是缓存空值(短 TTL)或者用布隆过滤器挡住非法 key。击穿指的是某个热点 key 恰好过期,大量并发请求同时打向数据库;解法是用互斥锁或者逻辑过期,让只有一个请求去重建缓存,其余请求短暂等待或返回旧值。

还有一个更危险的场景叫缓存雪崩:大量 key 在同一时刻集中过期,导致数据库在短时间内承受几十倍的读流量。最廉价的解法是给过期时间加随机抖动——比如原本统一的 3600 秒,改成 3600 到 4200 秒之间随机,把过期高峰摊平到一段时间里。这个改动只要一行代码,却往往能避免一次线上事故。

分布式缓存的另一个常被低估的问题是大 key 和热 key。一个几 MB 的 Hash 或者 List,在网络传输和序列化时会明显拖慢整个实例;而某个被疯狂访问的热 key,则可能把单个 Redis 分片的 CPU 打满,导致其他请求一起变慢。大 key 要拆,热 key 要在本地再加一层或做读副本,这些都是容量规划阶段就该想清楚的事。

第三层:缓存一致性,真正的难点从来不在「缓存」

如果说前两层解决的是「快」,那第三层解决的是「准」。缓存和数据库是两份数据,只要存在两份,就存在同步的窗口期,也就存在不一致的可能。工程上纠结的从来不是「能不能完全一致」,而是「能容忍多大的不一致,以及为此愿意付多少代价」。最常见的两种策略是先更新数据库再删除缓存和延迟双删,它们都以「删除」而非「更新」缓存为核心,因为删除是幂等的,不容易在并发下写进脏值。

但即使顺序正确,在极端并发下仍会出现经典的不一致:读请求在缓存未命中时读到旧值并写回缓存,而写请求的删除发生在这个读之前,于是旧值被重新写进缓存并长期驻留。这类问题的通用解法只有两条路——要么引入版本号/时间戳,让旧值无法覆盖新值;要么接受最终一致,用较短的 TTL 兜底,让脏数据在有限时间内自愈。

更稳妥的思路是把缓存当成可以随时丢弃的副本:数据库永远是唯一真相源,缓存丢了系统只是变慢,但不会出错。这条原则一旦确立,很多设计就自然清晰了——你可以放心地重启缓存、放心地缩短 TTL、放心地在故障时降级到直接查库,而不用半夜担心「缓存和库对不上了怎么办」。

深夜调试缓存问题的开发者桌面
深夜排查缓存问题:真正的难点从来不在缓存本身,而在一致性

结语:把缓存设计成「可降级」,而不是「不能没有」

回到开头那个场景:加一层 Redis 让指标变好,从来不是缓存设计的终点,而是起点。真正的缓存架构要回答三个问题——每一层存什么、每一层怎么失效、以及当某一层彻底挂掉时系统能不能扛住。

本地缓存追求极致的快,分布式缓存承担共享与流量闸门,一致性策略负责在「快」和「准」之间划一条可接受的边界。三层各就各位之后,缓存才不再是一个「加了就完事」的组件,而是一套能被主动驾驭、也能在故障时优雅退场的架构。下次再遇到缓存问题,不妨先问一句:这一层,到底是哪一层的责任?

💬 发表评论