site logo

Marico's space

第3天 / 30天:缓存,以及为什么失效真的那么难

Others 2026-07-08 11:27:43 14

最近踩了几天缓存的坑,发现这玩意儿看起来简单,真要用好,里面的门道比我想的深多了。这篇把第3天的学习整理一下,重点说说缓存失效为什么真的那么难。

回顾

第1天聊了大图(客户端-服务端、扩展性、延迟vs吞吐量),第2天进了数据库(SQL vs NoSQL、复制、分片)。今天的课题本来以为早就懂了:缓存。结果发现我只懂了最容易的那20%。

缓存为什么存在

核心思想很简单:取数据是有代价的(数据库查询、网络调用、计算),所以把结果复制一份存在访问更快、更便宜的地方,每次直接读这份副本,不用重复干活。

但"把副本存在某个更快的地方"背后藏着一堆决策:存哪儿存多久怎么知道这个副本已经不准了

缓存在哪儿

我梳理了一下典型系统中缓存出现的不同层级,比想象中多:

  • 客户端缓存 — 浏览器缓存、App缓存。根本不走网络。
  • CDN(内容分发网络)缓存 — 把静态资源(图片、脚本、视频)缓存在离用户最近的节点。
  • 应用层缓存 — 部署在应用服务器和数据库之间的内存存储,常见的是Redis或Memcached。
  • 数据库缓存 — 数据库内置的查询结果缓存或缓冲池。

我注意到一个规律:缓存离用户越近,速度越快,但越容易过期,而且失效越难控制。

缓存策略

几个反复出现的模式:

  • Cache-aside(懒加载) — 应用先查缓存,没命中再从数据库取,然后写入缓存。简单,只缓存真正被请求的数据,但第一次请求总是要付出完整代价。
  • Write-through(写穿透) — 数据同时写入缓存和数据库。保持缓存一致,但每次写入都增加延迟。
  • Write-behind(写回) — 数据立即写入缓存,异步写入数据库。写入快,但如果缓存在写入到数据库之前挂了就有风险。

没有哪个策略是放之四海而皆准的"正确答案"。Cache-aside是我默认会选的,但当数据陈旧不可接受时write-through更合适,当写入吞吐量比持久性保证更重要时write-behind更合适。

缓存失效:真正难的部分

这里那句著名的话才真正让我理解了:计算机科学里只有两件困难的事——缓存失效、命名、 Off-by-one 错误(这个笑话本身就是对命名和计数有多难的吐槽)。失效之所以难,是因为你要回答"真实数据源变了吗",又不能一直去检查真实数据源,否则缓存就失去意义了。

我研究的几种策略:

  • TTL(生存时间) — 缓存条目到期就过期。简单、可预测,但总要在新鲜度和缓存命中率之间做取舍。
  • 显式失效 — 底层数据变化时,应用主动通知缓存删除或更新对应条目。更精准,但每条写路径都要和缓存层耦合。
  • 事件驱动失效 — 变更发布一个事件(比如通过消息队列),监听的缓存自行失效。多个服务间扩展性更好,但增加了基础设施,而且引入了一种新的失败模式:事件万一没送到怎么办?

缓存雪崩——之前没考虑过的失败模式

一个之前没太注意的问题:当一个热门缓存条目过期瞬间,大量请求同时穿透到数据库——因为它们同时没命中缓存。这叫缓存雪崩(也叫惊群效应),是个很好的例子:缓存本来是保护数据库的工具,但如果不注意,反而会在最糟糕的时刻给数据库最大压力。常见的缓解方案包括过期时间随机化,或者"锁"机制——只有一个请求去重建缓存,其他请求等着。

今天真正想明白的事

缓存表面看是性能优化,但实际上是个穿着性能优化外衣的一致性问题。每个缓存决策本质上都是在问:你愿意让数据有多陈旧、陈旧多久,来换取速度?

这和第1天(CAP定理)和第2天(复制)遇到的取舍问题是一模一样的形状。开始感觉系统设计的大部分内容就是少数几种取舍反复穿着不同的马甲出现。

明天

下一个课题:负载均衡 —— 流量是怎么真正分配到多台服务器的、不同的负载均衡算法、以及如果不小心负载均衡器本身会成为单点故障。

如果你在生产环境被缓存失效bug坑过,欢迎分享——这类教训往往是最难忘的。