Cache-Control 是缓存行为的总开关。它的指令分四族:能不能存、能存多久、过期了怎么办、给谁存。
从这一节开始逐个讲通用首部字段。第一个就是最复杂、也最实用的这个。
一、四族指令
| 族 | 指令 | 管什么 |
|---|---|---|
| 能不能存 | no-store、no-cache |
存不存、用前要不要问 |
| 存多久 | max-age=N、s-maxage=N |
有效期多少秒 |
| 过期怎么办 | must-revalidate、stale-while-revalidate |
能不能拿旧的凑合 |
| 给谁存 | public、private |
共享缓存许不许存 |
八个指令里最容易混的还是 no-store 与 no-cache:前者「一个字节都不许留」,后者「可以留, |
||
| 但每次用之前必须问」。 | ||
s-maxage 是 max-age 的共享缓存版本。 |
||
CDN 想缓存久一点,但浏览器应该很快过期时,就写 s-maxage=3600, max-age=60:CDN 存一小时, |
||
| 浏览器只存一分钟。 |
二、must-revalidate 管的是「断网时能不能凑合」
没有这个指令时,一个已经过期的副本在连不上源服务器的情况下,某些缓存会直接把它发出去,
当作降级可用。
加上 must-revalidate,就明确禁止:宁可报错,也不给旧货。
这对金融、库存这类数据是必须的——一份过期的余额比一个错误更危险。
与之相反的是 stale-while-revalidate=30:过期后还允许 30 秒的宽限,这 30 秒内先返回旧的,
同时后台去更新。用户体验上是「永远不等待」,代价是可能看到 30 秒前的数据。
三、private 与 public 分的是「谁能存」
public 表示「谁都能存」,包括中间的 CDN 和代理。
private 表示「只许最终用户的浏览器存」,中间的共享缓存不许存。
这条指令经常被漏写,后果很严重:一个带用户信息的响应如果不写 private,CDN 缓存下来之后,
下一个请求同 URL 的人就会拿到别人的数据。
稳妥的做法是:默认给所有需要登录才能看的响应加 private,不确定的时候宁可加。
四、动手 Demo:八条指令,逐条判一次
假设缓存是在 0 秒时存下的,把时钟拨到不同时刻, 用八条不同的 Cache-Control 各判一次「能不能直接用」。
def judge(cc, now, stored_at):
c = cc.lower()
if u'no-store' in c:
return u'不存'
if u'no-cache' in c:
return u'存,但每次验证'
if u'max-age=' in c:
lim = int(c.split(u'max-age=')[1].split(u',')[0])
age = now - stored_at
if age < lim:
return u'直接命中'
if u'stale-while-revalidate=' in c:
w = int(c.split(u'stale-while-revalidate=')[1].split(u',')[0])
if age < lim + w:
return u'过期,先给旧的'
if u'must-revalidate' in c:
return u'过期,不许凑合'
return u'过期,可验证'
实跑输出:
--------------------------------------------------------------------
Cache-Control 的八条常用指令,逐条判一次
--------------------------------------------------------------------
假设缓存是 0 秒时存的,现在到了表里那个时刻
指令 现在 判定 说明
------------------------------------------------------------------------------
max-age=600 60 秒 直接命中 剩 540 秒
max-age=60 120 秒 过期,可验证 超 60 秒
no-store 0 秒 不存
no-cache 0 秒 存,但每次验证
private, max-age=600 60 秒 直接命中 剩 540 秒
public, max-age=600 60 秒 直接命中 剩 540 秒
max-age=60, must-revalidate 120 秒 过期,不许凑合 超 60 秒
max-age=60, stale-while-revalidate=30 120 秒 过期,可验证 超 60 秒
能存下来的 除 no-store 外都可以
一次都不用问的 只在有效期内成立,过期一定要问
最严的组合 no-store > no-cache > max-age + must-revalidate
最后一行那个 stale-while-revalidate:60 秒到期后还给了 30 秒宽限,
这 30 秒内可以先返回旧内容,同时后台去更新——用户感受不到等待。
private 和 public 分的是「谁能存」:private 只许最终用户的浏览器存,
中间那些 CDN 和代理不许存。带用户信息的响应不写 private,
第一个请求它的人就可能把它缓存下来发给别人。
public 反过来,明确允许共享缓存存,常用于不带身份的内容。
max-age=600 在 60 秒时命中,剩 540 秒;max-age=60 在 120 秒时已经过期 60 秒。
no-store 判定是「不存」,no-cache 是「存,但每次验证」——这两个在表里看起来像,实质完全不同。
private, max-age=600 和 public, max-age=600 在这一列里表现一样,
因为「谁能存」不影响「还能不能用」。它们的差别在多级缓存里才显现。
max-age=60, must-revalidate 在 120 秒时是「过期,不许凑合」,
而 max-age=60, stale-while-revalidate=30 在 120 秒时是「过期,
先给旧的」——因为 120 减 60 等于 60,还在 30 秒宽限之内吗?不在。
宽限是到 90 秒为止,所以它落在「过期」这一档。
30 秒实验
- 把
max-age=60换成s-maxage=3600, max-age=60,想想 CDN 和浏览器各存多久。 - 把
stale-while-revalidate=30改成 90,重跑,看 120 秒那一行会不会变。 - 想一想:为什么一个「登录后才能看」的接口,只写
max-age=60是不够的?
常见误区
- 「
no-cache是不缓存」——它允许缓存,只是每次用前必须验证。 - 「
max-age对所有缓存都生效」——共享缓存要用s-maxage才能覆盖它。 - 「
private是加强隐私的」——它只是限制「谁能存」,不做任何加密。
自测三问(点开对答案)
1. `no-store` 和 `no-cache` 的区别? —— `no-store` 不许保存;`no-cache` 可以保存,但每次使用前必须向源服务器确认。 2. `s-maxage` 和 `max-age` 有什么区别? —— `s-maxage` 只对共享缓存(CDN、代理)生效,且优先级高于 `max-age`。 3. `must-revalidate` 解决什么问题? —— 禁止缓存在连不上源服务器时,把已经过期的副本当降级内容发出去。Cache-Control 管缓存,下一个字段管连接——它也顺手管了逐跳标记。
- ☐ (本节完,下一节 6.3.2 Connection)