# 凡是能记住的，就别再算一遍

关于 caching 的速成课。20 个概念——cache hit 与 miss、hit ratio、CDN、Redis、TTL、cache invalidation、cache-aside、write-through、LRU、cache stampede 等——每个都有英文名称、一幅图、一个例子和一张卡片。从浏览器和 CDN 到内存存储和 CPU。结尾附一页速查表，列出全部概念。

- **Author:** Fedor Chishchin
- **Level:** intro
- **Reading time:** ~30 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/caching-express

---

## 1. cache 是什么

cache 只是一个简单的想法：把答案的一份副本放在某个快的地方，这样同一个答案不用付两次代价。懂了这一点，你以后遇到的每个 cache 都是同一个把戏，只是换了个地方。

### 地图：七组共 20 个概念

> 一张地铁图：先看到所有线路的全貌，再一站一站地坐过去。

先是三个**基础**——cache 是什么、hit 与 miss、以及它为什么管用。然后是六组，每章一组：cache 的**层级**、**hit ratio**、**新鲜度**、更新与淘汰的**策略**、**陷阱**，以及如何**用好它**。

后面每个概念都是同一个样子：它的**英文名称**（要记住——这就是术语）、一幅生活里的画面、一张**图**，以及一张**卡片**，字段有*问题*、_思路_、_例子_、_代价_、*何时使用*和*关联*。

_Diagram: 基础在上，然后是六组——第 2–7 章依次讲它们。_

### Cache — cache 记住答案

> 桌上放一本便笺，旁边是一个很慢的文件柜——查过一次的东西你就记下来，下次读便笺就行，不用再穿过房间。

**cache** 是一个又小又快的存储，保存着耗费很大的工作的结果——一次数据库查询、一个算好的页面、一个下载的文件——这样下一次请求就很便宜。昂贵的事只做一次；便宜的读取做很多次。这就是全部原理。其余都是细节：便笺放在哪里，以及什么时候该信上面写的东西。

_Diagram: 应用先问 cache。只有答案不在时，才去找慢速的数据源。_

#### Cache (文件柜旁的便笺)

- **问题:** 同样昂贵的工作被一遍又一遍地做。
- **思路:** 把结果存在某个快的地方，从那里读取。
- **例子:** 一个算好的页面、一次数据库查询的结果、一个下载的文件——都存在 cache 里。
- **代价:** 副本占用内存，并且有副本过时的风险。
- **关联:** 课程的其余部分讲的就是 cache 放在哪里，以及什么时候该信它们。

### Cache hit 与 cache miss — 两种结果

> 你去拿便笺。要么它就在那里，你立刻读到——要么它是空白的，你终究还是得走到文件柜那里。

答案在 cache 里时，叫 **cache hit**（命中）——又快又便宜。不在时，叫 **cache miss**（未命中）——你要做缓慢的工作，通常还会把结果存起来，让下次命中。这两个词描述了 cache 的每一次交互。caching 这场游戏，就是把 miss 变成 hit。

_Diagram: hit 立刻作答。miss 要走到数据源那里，并把结果存起来留给下次。_

#### Cache hit / miss (找到 / 没找到)

- **是什么:** hit 表示答案在 cache 里。miss 表示不在。
- **如何运作:** miss 时你做缓慢的工作，并把结果放进 cache——这样下次就是 hit。
- **例子:** 第一次访问某个页面：一次 miss，加一次数据库查询。第二次访问：毫秒级的 hit。
- **关联:** hit 所占的比例就是 **hit ratio**，是 cache 的主要成绩。

### Locality — 它管用是因为重复

> 咖啡店记住老顾客点的东西——值得这么做，只因为同样的人一次又一次点同样的东西。

当同样的答案被反复需要时，caching 才划算——而计算里几乎一切都在重复。这个性质叫 **locality**（局部性）：访问不是随机的，而是集中在少数**热门**条目上。同一个热门页面、同一个用户的资料、同一次查找，被问上千次。所以，一个装着热门条目的小 cache 就能吸收极大一部分工作。没有重复，就没有理由做 cache。

_Diagram: 少数热门条目承接了大部分请求；长尾部分很少被问到。_

#### Locality (热门集合)

- **是什么:** 同样的数据被一遍又一遍地请求。
- **为什么重要:** 一个装着热门条目的小 cache 能接住极大一部分请求。
- **例子:** 一个热门页面、同一个用户的资料、同一个搜索。
- **陷阱:** 没有重复——cache 几乎不命中，只是白白浪费内存。
- **关联:** locality 让高 **hit ratio** 成为可能。

> **定调的问题:** "这个答案生成起来贵吗，并且被反复请求吗——这两个条件正是让记住它值得费这番功夫的原因？"

> cache 保存一份昂贵答案的快速副本。hit 是读便笺；miss 是走去文件柜。要做的事，就是把 miss 变成 hit。

## 2. cache 无处不在

caching 不是某个地方的某一件事——它是在用户与数据之间的每一层都重复出现的模式。副本离需要它的人越近，就越快。

### Cache Hierarchy — cache 越近，hit 越快

> 你的用品放在桌上、房间里、楼里，以及城那头的仓库里——越近拿得越快，但能放的越少。

用户与真相之间有一级一级的 cache——一个 **cache hierarchy**（缓存层级）：**浏览器**把文件存在你的设备上，**CDN** 把副本存在你所在城市附近，**应用**把热门数据放在内存里，**CPU** 有只隔几纳秒的微型 cache。越近的 cache 越快但越小。请求先试最近的，miss 时再向外退一层。

_Diagram: 请求从最近的 cache 向外走。越近越快，但越小。_

#### Cache Hierarchy (多层 cache)

- **问题:** 一个 cache 不可能同时又近、又快、又大。
- **思路:** 一串 cache：近的小而快，远的大而慢一些。
- **例子:** 浏览器 → CDN → 应用 cache（Redis）→ 数据库。
- **代价:** 过时的副本多了更多可以藏身的地方。
- **关联:** 这些层是 **CDN**、**内存 cache** 和 CPU cache。

### In-memory Cache (Redis) — 主力

> 一个职员把最常被要的文件放在桌上伸手可及的地方，而不是每次都跑到地下室档案库去取。

大多数应用会在应用与数据库之间放一个专门的**内存 cache**——像 **Redis** 或 **Memcached** 这样的工具。内存比磁盘快得多，所以把热门结果放在那里，就能把一次缓慢的数据库查询变成近乎瞬时的读取。人们说"加个 cache"来加速后端时，通常指的就是这个。

_Diagram: Redis 把 key → value 键值对存在内存（RAM）里。只有 miss 时，应用才去访问磁盘上的数据库。_

#### In-memory Cache (Redis、Memcached)

- **问题:** 磁盘上的数据库查询很慢，而且相同的查询会重复出现。
- **思路:** 把热门结果存在内存里，按 key 查找。
- **例子:** key `user:42` 存着一份资料；应用从 Redis 而不是数据库读取它。
- **代价:** 多了一个服务；内存有限；副本可能过时。
- **何时使用:** 在慢速的真相来源前面加速后端。
- **关联:** 通常用 **cache-aside** 模式来填充。

### CDN — 缓存在用户身边的网络

> 热门图书摆在各地的本地图书馆里，这样谁都不必向唯一的中央档案馆邮购。

**CDN**（content delivery network，内容分发网络）是把 caching 用到地理上：它把你的页面、图片和文件的副本存在世界各地的服务器（**edge** 边缘服务器）上，让每个用户从附近的一台获得服务。这就是为什么一个全球性的网站在各处都加载很快，而不只是在离它的**源**（origin）服务器近的地方。还是同一个 hit/miss 的想法——只不过副本散布在地图上，以此战胜距离。

_Diagram: 用户从最近的 edge 拿到副本。edge 只有在 miss 时才去找 origin。_

#### CDN (内容分发网络)

- **问题:** 服务器在某一个国家，远处的用户要为距离等待。
- **思路:** 把副本存在遍布世界的 edge 服务器上。
- **例子:** 一个网站的图片从离用户近的城市提供，而不是从 origin 提供。
- **代价:** 散布在世界各地的副本同样会过时；需要有办法刷新它们。
- **何时使用:** 面向多地用户的静态文件、页面和资源。
- **关联:** **cache hierarchy** 的其中一层。

> **层级的问题:** "考虑到谁在请求它、我能把它放到离他们多近的地方，这个答案应该缓存在哪一层——浏览器、CDN、应用内存，还是数据库？"

> caching 是在每一层重复出现的模式：浏览器、CDN、内存存储、CPU。越近越快但越小——而 miss 会落到外面的下一层。

## 3. hit ratio 才是关键

只有当 cache 真的接住了大多数请求，它才配得上自己带来的复杂度。一个数字——hit ratio——就能告诉你，你的 cache 有没有物有所值。

### Hit Ratio — 真正重要的成绩

> 守门员是看他扑出的射门占多大比例来评判的。扑出 95% 的守门员能改变一切；只扑出 10% 的，几乎改变不了比赛。

**hit ratio**（命中率）是由 cache 而不是慢速数据源来响应的请求所占的比例。95% 的 hit ratio 意味着每二十个请求里只有一个会到达数据库——负载和延迟都大幅降低。10% 的 hit ratio 则意味着这个 cache 基本上是额外开销。判断一个 cache 是否管用，靠的就是这一个数字。

_Diagram: 95%：每二十个请求里一个到达数据库。10%：cache 几乎什么也没接住。_

#### Hit Ratio (命中占比)

- **是什么:** hits ÷ (hits + misses)——由 cache 响应的请求所占的比例。
- **例子:** 95%——每 20 个请求里 1 个到达数据库。10%——cache 几乎没帮上忙。
- **原因:** 判断 cache 是否管用的主要方法，也是你要调的主要旋钮。
- **关联:** 随 **locality** 和合理的**淘汰**而升高；随**冷 cache** 和较短的 **TTL** 而下降。

### Cold 与 Warm Cache — 冷 cache 需要预热

> 一家货架空空的新店，一开始谁也服务不快——随着顾客开口要东西才慢慢进货，直到热门商品总是现成的。

刚启动之后，cache 是**冷**的（cold）——空的，所以每个请求都 miss，都去找慢速的数据源。随着真实流量涌入，它被热门答案填满，变得**热**（warm），hit ratio 随之攀升。这就是为什么刚重启或部署后性能可能看起来很差，然后又稳定下来。有些系统会有意地**预热**（pre-warm）cache，在用户到来之前就加载已知的热门数据。

_Diagram: 重启之后，hit ratio 从零开始爬升。预热过的 cache 一开始就很高。_

#### Cold / Warm Cache (预热)

- **是什么:** 冷——空 cache，全部 miss。热——已填满，能命中。
- **例子:** 部署之后，网站会慢几分钟，直到 cache 被填满。
- **陷阱:** 同时重启所有节点，会把全部负载压到数据库上。
- **对策:** **预热**：在用户到来之前加载热门数据。
- **关联:** 解释了重启之后 **hit ratio** 的下跌。

### Memory–Speed Trade-off — cache 用内存换速度

> 租一张更大的桌子，让更多文件放在伸手可及的地方——占的空间更多，但你去文件柜的次数少得多。

caching 不是免费的：你花的是有限且要花钱的**内存**，来省下**时间**。cache 越大，存得越多，命中也越频繁，但你不可能什么都缓存——所以窍门在于缓存*对的*东西：那些热门的、昂贵的、被反复询问的答案，它们每存一个字节换来的速度最多。cache 是一次刻意的**取舍**（trade-off）。

_Diagram: 你付出内存，买到时间。hit ratio 告诉你这笔交易划不划算。_

#### Memory–Speed Trade-off (拿内存换时间)

- **思路:** 用内存付费，以减少去慢速数据源的次数。
- **例子:** 给 Redis 更多内存——能放下更多 key，更多 hit。
- **代价:** 内存有限且要花钱；你不可能什么都缓存。
- **何时使用:** 缓存每字节换来速度最多的东西：热门的和昂贵的。
- **关联:** **hit ratio** 衡量回报；**淘汰**腾出空间。

> **命中率的问题:** "这个 cache 实际上在响应多大比例的请求——这个比例够高，值得它占用的内存吗？"

> hit ratio 是 cache 的成绩单。你花内存买速度——所以要缓存那些热门、昂贵、每字节换来最多 hit 的答案。

## 4. 让它保持新鲜

一个记下来的答案，可能在你还信着它的时候就已经过时。知道什么时候该不再相信那份副本，是 caching 中核心的、真正困难的问题。

### Stale Data — caching 的代价

> 你记在便利贴上的那个电话号码是宝贝——直到朋友搬了家，那张便条悄悄变成了错的。

记住一个答案的另一面是：真实的答案可能变了，而你的副本没变。**stale**（陈旧）数据就是不再与真相相符的缓存值——旧价格、已删除的帖子、一小时前的库存数。每个 cache 都有提供陈旧数据的风险，而决定你能容忍多少陈旧，是任何 cache 的第一个真正的设计问题。

_Diagram: 数据源里的价格变了，但没人更新缓存的副本——它变陈旧了。_

#### Stale Data (过时的副本)

- **是什么:** 一个不再与数据源相符的缓存值。
- **例子:** 旧价格、已删除的帖子、一小时前的库存数。
- **为何发生:** 真相变了；副本没变。
- **关联:** 受 **TTL** 和**失效**约束；能容忍多少，由 **staleness budget** 决定。

### TTL — 让它按计时器过期

> 牛奶上盖着保质期——过了那天，不检查就不会相信它，哪怕它看起来还好好的。

最简单的新鲜度工具是 **TTL**（time to live，存活时间）：每个缓存条目都有一个过期时间，过了之后该条目就被当作不存在，迫使重新获取新数据。TTL 短意味着数据更新鲜但 miss 更多；TTL 长意味着 hit 更多但更陈旧。选 TTL，就是在新鲜度与速度的权衡里选出你的确切位置——按每种数据分别选，依据是它实际变化得有多快。

_Diagram: TTL 用完之前，答案来自 cache。之后，下一个请求去获取新数据。_

#### TTL (存活时间)

- **问题:** 副本可能变陈旧，而 cache 并不知道。
- **思路:** 给每个条目一个有效期；过了之后，重新获取。
- **例子:** 汇率用 60 秒的 TTL 缓存：永远不会比一分钟更旧。
- **代价:** TTL 短意味着更多 miss；TTL 长意味着更陈旧。
- **何时使用:** 几乎总是用，作为基本的安全网；按每种数据调整。
- **关联:** 一个热门 key 对所有人同时过期，会引发 **cache stampede**。

### Cache Invalidation — 为什么它出了名的难

> 在世界某处某个事实变化的那一刻就知道，好在恰到好处的时机撕掉你的便条——说起来容易，做起来让人发疯。

有个流传已久的笑话说，计算机科学里只有两个难题，而 **cache invalidation**（缓存失效）——知道什么时候必须丢掉某个缓存值——就是其中之一。它之所以难，是因为 cache 往往不知道底层数据已经变了；变化发生在别的地方。下一章讲的就是人们用来对付这个难题的一套策略。

_Diagram: 一个服务修改了数据库，另一个服务从 cache 读到旧副本。谁来通知 cache？_

#### Cache Invalidation (丢掉副本)

- **问题:** 数据在别处变了，而 cache 不知道。
- **思路:** 真相变化时，丢掉或刷新副本。
- **例子:** 价格变动之后，从 cache 里删掉该商品的 key。
- **代价:** 你得找出副本存在的每一个地方，一个都不能漏。
- **关联:** 策略：**cache-aside**、**write-through**、**write-around**；安全网：**TTL**。

> **新鲜度的问题:** "这份数据安全地陈旧到什么程度都行——简单的 TTL 够用，还是在真相变化时我需要主动丢掉副本？"

> 每个缓存的答案都可能变陈旧。TTL 用计时器让它过期——而确切知道何时失效，是计算里真正困难的问题之一。

## 5. 失效与淘汰

两股力量决定 cache 里有什么、它是否可信：真相变化时你如何更新它，以及它满了时你如何腾出空间。两者都有值得了解的、有名字的策略。

### Cache-aside — 由应用来填充 cache

> 你先看便笺；如果是空白，就走到文件柜，读完文件，把答案记下来，再继续手头的事。

最常见的模式是 **cache-aside**（也叫 **lazy loading**，懒加载）：读取时，应用先查 cache；miss 时，从数据源获取，存下结果并返回。cache 按需填充，存的正是实际被请求的东西。它简单而流行——它的弱点是陈旧，因为一个值会一直待在 cache 里，直到有东西让它过期或失效。

_Diagram: cache-aside 一步一步：问 cache → miss → 读数据库 → 存入 cache → 返回。_

#### Cache-aside (lazy loading)

- **问题:** 你需要填充 cache，却事先不知道会被问到什么。
- **思路:** 由应用来做：查 cache，miss 时读数据库，并把结果放进 cache。
- **例子:** `GET user:42` → miss → 对数据库 `SELECT` → 带 TTL 地 `SET user:42`。
- **代价:** 一个值可能一直陈旧，直到它的 TTL 用完或有人删除它。
- **何时使用:** 数据库前面大多数 cache 的默认做法。
- **关联:** 常与 **TTL** 搭配；写入时用 **write-through** 来补充。

### Write-through 与 Write-around — 让写入保持一致

> 一个事实变化时，你可以在更新总档案的同时更新桌上的便条——也可以跳过便条，让它下次重新获取新的。

数据被*写入*时，你要选择 cache 怎么跟上。**Write-through** 同时更新 cache 和数据源，所以 cache 永远不会陈旧（代价是写入更慢）。**Write-around** 只写数据源，让 cache 在 miss 时再重新加载，避免缓存没人会再读的数据。选哪个，取决于你的读写比例。

_Diagram: Write-through 同时写 cache 和数据库。Write-around 只写数据库；cache 在 miss 时跟上。_

#### Write-through / Write-around (写入策略)

- **问题:** 写入时，cache 和数据源会各走各的。
- **思路:** Write-through——cache 和数据库一起更新。Write-around——只写数据库。
- **例子:** 马上就会被再次读取的资料——write-through。几乎没人读的日志——write-around。
- **代价:** Write-through——写入更慢。Write-around——写入之后紧接着一次 miss。
- **何时使用:** 取决于你的读写比例。
- **关联:** 从写入一侧做 **cache invalidation** 的方式。

### Eviction (LRU, LFU) — 满了的时候决定丢什么

> 一张已经放满的小桌子——要放进新文件，就得收起一个，所以你拿走那个好久没碰过的，而不是每小时都用的。

cache 的空间有限，所以满了的时候必须**淘汰**（evict）一些东西。最常见的策略是 **LRU**（least recently used，最近最少使用）：丢掉最久没被碰过的，赌的是它不会很快被需要。还有别的策略——**LFU**（least frequently used）丢掉使用频率最低的。淘汰让一个小 cache 能自动保留热门条目、舍弃冷门条目，而不必无限增长。

_Diagram: cache 满了，F 来了。按 LRU，B 被淘汰——它最久没人碰。_

#### Eviction (LRU、LFU)

- **问题:** cache 空间用完了，而新数据还在不断到来。
- **思路:** 丢掉最不可能很快被需要的。
- **例子:** LRU——丢掉最久没碰的。LFU——丢掉用得最少的。
- **代价:** 有时会丢掉马上就要被问到的东西——多一次 miss。
- **关联:** 在不增加内存的情况下保持 **hit ratio**。和**失效**不是一回事：失效针对的是错误数据，淘汰针对的是空间。

> **策略的问题:** "底层数据变化时，这个 cache 怎么知道——而它满了的时候，丢掉的是真正冷门的条目吗？"

> 失效决定一个缓存值何时是错的；淘汰决定空间用完时丢什么。cache-aside、write-through、LRU——都是这两件事的名字。

## 6. caching 咬人的时候

caching 的威力伴随着锋利的棱角。那些经典的故障并不稀奇——它们可预测、有名字，而了解它们就是避开它们的一半。

### Cache Stampede — 热门 key 过期的时候

> 一件热门商品卖光了，就在那一刻，一百个顾客同时冲向柜台要它——把唯一负责补货的店员压垮了。

当一个热门缓存条目过期时，所有想要它的请求都会*在同一时刻* miss，并一起冲向慢速的数据源——这叫 **cache stampede**（缓存踩踏，也叫 **thundering herd**，惊群）。被保护了几个小时的数据库，突然一下子要面对整个人群，可能会被压垮。解决办法是已知的——让一个请求去刷新而其他请求等待，或者错开过期时间——但你必须预先想到：stampede 恰恰出现在流量最高的时候。

_Diagram: 一个热门 key 过期了——所有请求同时冲向数据库。下方：两种已知的解决办法。_

#### Cache Stampede (thundering herd)

- **问题:** 一个热门 key 过期，所有请求都 miss，同时冲向数据库。
- **例子:** 首页在流量高峰时过期——成千上万个请求同时重建它。
- **对策:** 一个请求去刷新，其余等待；或者给 TTL 加随机分散（jitter）。
- **何时要想到:** 每一个带 **TTL** 的热门 key。
- **关联:** **TTL** 用在热门数据上的副作用。

### Stale Reads — 又快、又自信、还是错的

> 从一张旧便利贴上读出一个价格，并自信满满地报出来——数字瞬间送到，却是错的数字。

cache 会乐呵呵地又快又充满自信地提供一个**陈旧**的值——这是一次 **stale read**（陈旧读取）。通常这没问题——浏览次数稍微旧一点，不会伤到谁。但对于正确性至关重要的数据——银行余额、库存、权限——一次陈旧读取可能就是真正的 bug。关键的本事是知道哪些数据能容忍陈旧、哪些必须永远新鲜，并且不要随随便便缓存后一类。

#### Stale Read (提供过去的数据)

- **问题:** cache 又快又自信地提供了一个旧值。
- **无害:** 一个晚了一分钟的浏览次数。
- **真正的 bug:** 银行余额、库存、权限。
- **对策:** 事先决定哪些数据能容忍陈旧，哪些不能。
- **关联:** **stale data** 是如何表现出来的；用 **staleness budget** 来处理。

### What Not to Cache — 有些东西根本不该缓存

> 你不会去复印一份每分钟都在改写的文件，也不会在共用的桌子上放别人的私人信件的复印件——这份副本要么是错的，要么本就不该在那里。

不是所有东西都属于 cache。**不断变化**的数据，hit ratio 很差，还频繁陈旧。**敏感的或按用户区分的**数据，如果共享 cache 把一个用户的副本交给另一个用户，就有泄露的风险。而**很少被请求**的数据只会浪费空间。知道*不*该缓存什么，和知道该缓存什么一样重要。

_Diagram: 三类不属于 cache 的数据。下方：共享 cache 如何把用户 A 的数据交给用户 B。_

#### What Not to Cache (跳过清单)

- **变化快:** hit ratio 很差，而且持续陈旧。
- **敏感:** 共享 cache 里按用户区分的数据和机密数据可能到达错误的用户手里。
- **冷门:** 很少被请求——只会占空间。
- **关联:** **cache target** 的另一面。

> **陷阱的问题:** "一个热门条目过期的那一刻，我的慢速数据源会怎样——这个 cache 里有没有什么东西，对新鲜度要求太高或太敏感，根本不该放在这里？"

> caching 经典的咬人之处都有名字：热门 key 过期时的 stampede、带着虚假自信提供的 stale read，以及缓存了根本不该缓存的东西。

## 7. 用好它

caching 是一件强力工具：用一点点内存换来巨大的速度，代价是有提供过去数据的风险。有意识地使用，它是计算里最好的交易之一。

### Cache Target — 缓存热门、昂贵、变化慢的东西

> 你记住老顾客的常点，和一天要给十遍的路线——而不是一次性的请求，或者按分钟变化的东西。

理想的 **cache target** 是**经常被读取、生成起来昂贵、很少变化**的东西——这种组合以最小的陈旧风险换来最大的速度。一个由慢查询构建、一整天重算结果都一样的热门页面，是完美的。一个不断变化、很少被读取、又敏感的值，则正相反。

_Diagram: 三个条件同时满足，就是理想的 cache target。_

#### Cache Target (该缓存什么)

- **思路:** 经常被读 + 生成昂贵 + 很少变化。
- **例子:** 一个由慢查询构建、一整天都一样的热门页面。
- **反例:** 一个不断变化、很少被读取、又敏感的值。
- **关联:** **what not to cache** 的反面；它带来高 **hit ratio**。

### Staleness Budget — 事先就定好

> 事先约定多旧是可以接受的——天气小组件可以晚几分钟；银行余额一秒都晚不得。

在缓存任何东西之前，先决定它能容忍多少**陈旧**——这就是它的 **staleness budget**（陈旧预算）。这个选择决定了其他一切——TTL、你是否需要主动失效、它到底能不能安全地缓存。要按每种数据把它明确下来。"它被允许错到什么程度，错多久？"这个问题，能把 caching 从神秘 bug 的来源，变成一笔受控的、刻意的交易。

_Diagram: 左边是能容忍陈旧的数据；右边是必须永远新鲜的数据。_

#### Staleness Budget (多旧才算可以)

- **问题:** 没有明确的决定，陈旧就会以神秘 bug 的形式出现。
- **思路:** 对每种数据，事先决定它可以有多旧。
- **例子:** 天气——几分钟没问题。银行余额——一秒都不行。
- **它决定什么:** **TTL** 的长度、是否需要**失效**、到底要不要缓存。
- **关联:** 应对 **stale data** 和 **stale read** 风险的答案。

### 这些概念如何联系起来

> 天平的两个托盘：一边是速度，一边是新鲜度。本课的每个概念都在移动其中一个。

整个 caching 就是**速度**与**新鲜度**之间的拉锯。左边，是抬高 **hit ratio** 的因素：locality、热 cache、合理的淘汰。右边，是压住 **stale data** 的因素：staleness budget 决定 **TTL**，而**失效**丢掉错误的数据。TTL 短，更新鲜但命中更少；在热门 key 上，同样的 TTL 可能引发 **stampede**。

_Diagram: 左边是速度的力量，右边是新鲜度的力量。预算决定 TTL；TTL 约束陈旧，也可能引发 stampede。_

#### 加 cache 之前

- **数据是热门的吗**——被反复请求的程度，足以带来真正的 hit ratio 吗？
- **生成起来昂贵吗**，让一次 hit 确实能省下可观的工作吗？
- **它能有多陈旧**——TTL 是多少，我需要主动失效吗？
- **热门条目过期时会怎样**——会引发 stampede 吗？
- **缓存它安全吗**——共享 cache 里没有按用户区分的机密数据吧？
- **我怎么知道它管用**——我在测量 hit ratio 吗？

#### 你缓存得好的迹象

- 你能说出重要 cache 的 **hit ratio**。
- 每个被缓存的东西都有一个**经过考虑的 TTL**，与它实际变化的快慢相匹配。
- 没有任何**按用户区分的或机密的**东西意外地躺在共享 cache 里。
- 你的热门 key 在过期时不会**一下子踩踏**数据源。
- 你缓存的是**热门、昂贵、变化慢**的答案——其余的就不管。

## 速查表：全部 20 个概念

### 基础

- **Cache** (便笺): 昂贵答案的快速副本，免得把活儿做两遍。
- **Cache hit / miss** (找到 / 没找到): 答案在 cache 里——或者你得去数据源。
- **Locality** (热门集合): 同样的数据被一遍又一遍地请求——这就是 cache 管用的原因。

### 层级

- **Cache Hierarchy** (多层 cache): 浏览器 → CDN → 应用内存 → 数据库。越近越快，但越小。
- **In-memory Cache** (Redis、Memcached): 慢速数据库前面放在内存里的热门数据。
- **CDN** (内容分发网络): 文件和页面的副本，放在离用户近的服务器上。

### 命中率

- **Hit Ratio** (命中占比): 由 cache 响应的请求所占的比例。cache 的主要成绩。
- **Cold / Warm Cache** (预热): 空 cache 全部 miss；填满的就能命中。预热会提前加载它。
- **Memory–Speed Trade-off** (拿内存换时间): 付出内存，买到时间。缓存每字节换来速度最多的东西。

### 新鲜度

- **Stale Data** (过时的副本): 副本不再与真相相符。
- **TTL** (存活时间): 一个条目的寿命；过了之后，重新获取。
- **Cache Invalidation** (丢掉副本): 真相变化时丢掉副本。出了名的难。

### 策略

- **Cache-aside** (lazy loading): 应用先查 cache，miss 时读数据库，并存下结果。
- **Write-through / Write-around** (写入策略): 同时写 cache 和数据库——或者只写数据库。
- **Eviction (LRU / LFU)** (腾出空间): 满了的时候，丢掉最久没碰的或用得最少的条目。

### 陷阱

- **Cache Stampede** (thundering herd): 一个热门 key 过期——所有人同时冲向数据库。
- **Stale Read** (提供过去的数据): 又快、又自信、还是错的。对金钱、库存、权限很危险。
- **What Not to Cache** (跳过清单): 变化快的、敏感的、冷门的。

### 用好它

- **Cache Target** (该缓存什么): 经常被读、生成昂贵、很少变化。
- **Staleness Budget** (多旧才算可以): 事先决定数据可以有多旧。它决定 TTL 和失效。

> caching 用一点内存换来巨大的速度，代价是有提供过去数据的风险。做得好，它就是刻意的：对的答案，配上你有意选定的 staleness budget。

速成课结束 · 7 章 · 20 个概念

接下来是练习：找出你做过的东西里最慢的重复操作——一次查询、一次 API 调用、一个算出来的页面——在它前面放一个带明确 TTL 的小 cache。然后观察 hit ratio 和延迟的变化。你一测量，这笔交易就变得真实了。但要把一个想法放在所有想法之上：caching 只是记住一个昂贵的答案，而不是把活儿重做一遍——而每个难点，其实都是同一个被诚实问出的问题：我记下的东西，什么时候不再是真的了？
