Nginx 是如何让你的缓存延期的

当 Nginx 使用 proxy cache 的文件作为响应时,它会更新其中的一些内容,比如 Date 响应头;但大部分响应头都不会得到更新,比如 Expires 和 Cache-Control。众所周知,Cache-Control 可以通过 max-age=xxx 或者 s-maxage=xxx 指令设置缓存的有效时间。跟 Expires 响应头不同,这一时间是相对的。假设上游服务器返回 Cache-Control: public; max-age=3600,那么 Nginx 会缓存该响应一小时。如果在这一小时到期之前,Client 访问了 Nginx,它会获取到同样的 Cache-Control 响应头,因此会再缓存多一小时。所以总体上该响应会被缓存两小时。

这听起来很让人惊讶。但仔细想想,其实也不算什么严重的问题。首先,当我们设置 max-age=3600 时,大多数情况下并不要求其严格地在一小时后过期。其次,这个算是一般的多层缓存固有的弊端:缓存数据的最大过期时间,取决于各级缓存 TTL 的总和。如果想要避免,你可以选择根据外层数据剩下的 TTL 设置当前 TTL;或者提供主动 purge 的操作,从最内层开始逐层清理数据。

当然,某些时候下,这一行为会带来一些问题。举个例子,假设我们开启了 proxy_cache_use_stale,在上游服务器出问题时使用过期的内容代替正常的响应。这种情况下,缓存只是作为一个临时救急的方案使用,我们并不希望 Client 多缓存更多的时间。否则会有上游应用的开发者抱怨,为何上游服务器已经正常了,用户刷新页面看到的还是旧数据。作为解决办法,我们可以在 Nginx 的 header filter 阶段,通过 Lua 代码或者 Nginx C module,把 Cache-Control: max-age=... 修改成 Cache-Control: no-cache。这么一来,Client 会在使用缓存之前先验证下,如果 Nginx 返回 304 状态码,那么该缓存会被继续使用;如果上游已经 OK 了且更新了响应,那么 Client 就会重新请求,避免使用过期的内容。

这里需要强调下,no-cache 并非如字面上的意义表示不缓存,而是要求 Client 在使用该缓存之前,需要先验证下被缓存的内容是否还是最新的。MDN 的说法是:

Forces caches to submit the request to the origin server for validation before releasing a cached copy.

对应的,RFC 7234 的说法:

The "no-cache" request directive indicates that a cache MUST NOT use
a stored response to satisfy the request without successful
validation on the origin server.

如果要想让 Client 不缓存响应的内容,按 MDN 上的说法,需要用 Cache-Control: no-cache, no-store, must-revalidatehttps://developer.mozilla.org...)。

仔细看了下 no-cache / no-store / must-revalidate 这三项指令的介绍,似乎 no-store 就能让 Client 不用这个缓存,因为 no-store 要求:

The cache should not store anything about the client request or server response.

另外 must-revalidate 要求在使用过期缓存前验证下该内容是否是最新的,而 no-cache 也是要求重新验证的,那为什么需要两个都一起用呢?

Google 搜索把我带到了这个 SO 问答:https://stackoverflow.com/que...。这个回答里面解释了为何不单单用 no-store:因为臭名昭著的 IE6 浏览器在处理 no-store 时有 bug。但可惜的是,这个回答没有给出这一论断的证据,比如 IE 的 bug report 之类。MDN 在给出 Cache-Control: no-cache, no-store, must-revalidate 这个例子的时候,也没有提及更多的上下文。这很像没有任何注释的老代码:我们不知道当初为何这么写,而把它删掉似乎不会带来什么问题。

相关推荐