源站文件改了,并不意味着每个已经保存响应的缓存都会收到通知。失效操作控制的是哪些已存副本还可以继续复用,而不只是更新磁盘上的内容。可靠的发布要区分到期、重新验证、定点移除以及使用新 URL:它们解决不同范围的问题,也没有哪一种能远程擦掉读者已经下载的所有字节。
到期不是广播通知
源站发生变化后,一份仍然新鲜的响应可能继续被合法使用。新鲜度由缓存寿命和当前年龄共同决定,年龄计算涉及缓存元数据与经过的时间,并非只从源站的 Date 字段开始倒计时。到期改变的是直接复用时必须满足的条件,既不保证存储介质立即删除条目,也不会让无人访问的缓存主动联系源站。
验证器同样不是内容变化的推送通道。请求需要重新验证时,正确的 ETag 才能帮助源站确认表示未变,或者返回替代正文。浏览器手上的响应还新鲜、没有理由回源时,新增验证器并不能强迫它询问。把源站策略改成 no-cache 或 no-store,也无法追溯修改浏览器之前已经收到的缓存指令。
先确定失效覆盖范围
RFC 9111 要求:缓存收到不安全方法对应的成功或重定向响应时,必须使目标 URI 的已存响应失效。这个义务只涉及该请求实际经过的缓存,不是向整个网络广播。通过另一条接口修改数据库,不会自动覆盖所有派生页面、查询参数变体、不同语言表示和搜索列表,应用需要识别这些真实依赖。
CDN 的清理接口也只管理服务商控制的副本,无法直接清空独立浏览器缓存。应按服务商的实际契约确认受影响的主机、路径、变体以及缓存标签。先让替代内容在源站可用,再发起清理、跟踪完成状态,并比较实际响应正文和缓存状态。接口已经接受清理请求,并不能单独证明它已传播,也不能证明源站已经能返回预期的新字节。
坏掉的样子
如果源站尚未更新就先清理边缘,下一次未命中可能立即把旧内容重新填回去。已经在途的回填也可能和失效操作竞争,具体结果取决于缓存实现。反复执行全站清理不能证明这些竞争已经解决,反而会让更多对象变冷、增加回源负担。应准确圈定受影响对象,并评估清理之后源站需要承担的请求量。
对于静态资产,应先发布带内容指纹的新文件名,再切换 HTML 或清单中的引用。在旧文档仍可能引用旧资产期间,旧文件也应继续可用。新 URL 可以避开旧缓存条目,但前提是读者先拿到更新后的引用文档。只生成一个新文件名,却不更新文档,或者文档本身仍被直接复用,都不会使现有读者自动转向新版本。
涉及机密信息删除时,边界还要更严格。删除源站文件或清理 CDN 不能撤销已经下载的字节,缓存策略也不等于访问授权。应从源头防止敏感响应进入不合适的缓存,而不是泄露后依赖清理挽救。对于紧急修复,要先定义“可见”的对象:新访问者、已经打开的标签页、所有曾下载的副本,不能共享同一种到达保证。
快速验证
准备 Python 3.10 或更新版本及 POSIX shell;实际运行使用 Python 3.14.4、CacheControl 0.14.4 和 Requests 2.34.2。安装下列固定版本后,用 "$invalidation_lab/venv/bin/python" 执行 Python 代码。程序在自动分配的回环端口启动一个 HTTP 服务,并建立两个互不共享存储的内存私有缓存;关闭环境代理,不访问外部服务器,最后关闭客户端、线程和服务。只有依赖安装需要联网。
invalidation_lab=$(mktemp -d)
python3 -m venv "$invalidation_lab/venv"
"$invalidation_lab/venv/bin/python" -m pip install CacheControl==0.14.4 requests==2.34.2
from collections import Counter
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
import requests
from cachecontrol import CacheControl
from cachecontrol.cache import DictCache
assets = {"/manual.txt": b"A"}
counts = Counter()
class Origin(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
counts[self.path] += 1
body = assets.get(self.path, b"missing")
status = 200 if self.path in assets else 404
self.send_response(status)
self.send_header("Cache-Control", "max-age=600" if status == 200 else "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
server = HTTPServer(("127.0.0.1", 0), Origin)
worker = Thread(target=server.serve_forever)
worker.start()
base = f"http://127.0.0.1:{server.server_port}"
caches = [DictCache(), DictCache()]
clients = []
try:
for cache in caches:
client = CacheControl(requests.Session(), cache=cache)
client.trust_env = False
clients.append(client)
def check(index, path, status, body, cached):
response = clients[index].get(base + path, timeout=3)
assert (response.status_code, response.content, response.from_cache) == (
status, body, cached
)
for index in range(2):
check(index, "/manual.txt", 200, b"A", False)
check(index, "/manual.txt", 200, b"A", True)
assert counts["/manual.txt"] == 2
print("warm: two independent caches hold A; origin requests=2")
assets["/manual.txt"] = b"B"
caches[0].delete(base + "/manual.txt")
check(0, "/manual.txt", 200, b"B", False)
check(1, "/manual.txt", 200, b"A", True)
assert counts["/manual.txt"] == 3
print("targeted invalidation: cache 0=B; cache 1=A")
check(1, "/manual.v2.txt", 404, b"missing", False)
assets["/manual.v2.txt"] = b"B"
for index in range(2):
check(index, "/manual.v2.txt", 200, b"B", False)
check(index, "/manual.v2.txt", 200, b"B", True)
assert counts["/manual.v2.txt"] == 3
check(1, "/manual.txt", 200, b"A", True)
print("new URL: 404 before publication, then both caches=B")
print("old URL: cache 1 still holds A")
finally:
for client in clients:
client.close()
server.shutdown()
worker.join()
server.server_close()
两名客户端各自第一次向源站获取 A,随后命中自己的缓存,所以初始回源总数是两次。把源站内容改成 B 后,只删除零号缓存中精确 URL 对应的条目:零号客户端重新取得 B,一号仍直接返回 A,不会产生新回源。此时旧 URL 的回源总数为三次。结果直接展示了定点移除的作用范围,并没有证明所有副本已经收敛。
新文件名第一次请求返回带 no-store 的 404,特意暴露了“先引用、后发布”的错误顺序。发布这个文件后,两名客户端都能取得并缓存 B;由于前面的错误响应不存储,它不会挡住这次读取。然而一号客户端再访问旧 URL,仍然命中 A。新旧 URL 是不同条目,发布新版本并不意味着旧条目已被擦除。
这是使用真实 HTTP 请求和缓存存储的局部实验,不是 CDN 清理实现,也没有测试浏览器行为、全球传播或在途回填竞态。CacheControl 的安全模型是私有缓存,不能把这段程序扩成多用户共享边缘。实验只证明明确操作过的条目会变化,其他缓存仍按各自收到的策略工作;上线验收必须另外检查真实服务商规则和实际请求链路。
延伸阅读
来源
- RFC 9111:使已存响应失效,不安全方法与失效范围,已核验 2026-09-07。
- CloudFront:清理与版本化文件名,清理边缘不会清空独立缓存,已核验 2026-09-07。
- CacheControl 用法,会话与独立缓存存储,已核验 2026-09-07。
- CacheControl 安全模型,私有缓存的适用限制,已核验 2026-09-07。