📘 第六章 HTTP首部
📄 一句话概括

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 秒实验

  1. 把 max-age=60 换成 s-maxage=3600, max-age=60,想想 CDN 和浏览器各存多久。
  2. 把 stale-while-revalidate=30 改成 90,重跑,看 120 秒那一行会不会变。
  3. 想一想:为什么一个「登录后才能看」的接口,只写 max-age=60 是不够的?

常见误区

  1. 「no-cache 是不缓存」——它允许缓存,只是每次用前必须验证。
  2. 「max-age 对所有缓存都生效」——共享缓存要用 s-maxage 才能覆盖它。
  3. 「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 管缓存,下一个字段管连接——它也顺手管了逐跳标记。

相关笔记