关于HTTP Cache介绍和使用总结

1,378 阅读10分钟

1.Http Cache是什么?

通过本地缓存(一般是浏览器实现 或专门的本地缓存代理)、代理缓存(CDN服务器、网络提供商等)达到针对“相同”request复用上一次repsone结果。

它的存在解决了以下问题:

  • 加快http响应速度,提升客户体验。
  • 减少客户端到服务端之间的网络交互。
  • 减轻了服务端的压力。

1.1 Cache分类

  • 私有缓存

    私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。
    通过在Repsone Header中设置如下参数,同时private参数能够拒绝客户端到服务提供端中间的代理服务进行缓存。

Cache-Control: private
  • 共享缓存

    又分为以下两种:

    • 代理缓存
      随着近年来 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能隧道响应而不能充当缓存。
      PS:这个现在用的少,不细究。

    • 托管缓存
      托管缓存由服务开发人员明确部署,以减轻服务器压力为目的同时高效的响应服务内容。示例包括反向代理、CDN 和Service woker与Cache API 的组合。

      使用方式取决于专门的托管服务商,如Google、Amazon都提供了类似缓存服务,使用时依据服务商的Api文档结合Cache-control标头来对缓存进行管理控制。

1.2 默认缓存机制

当Respone中没有配置Cache-Control字段时,浏览器会使用名为启发式缓存模式“Heuristic caching”来进行缓存管理。

例,以下请求资源在服务端1年时间未被修改过,此次请求过后浏览器会默认将此资源的缓存时间制定为10% * 1年,在此时间内请求此资源不再请求源服务器而是从本次缓存获取(具体时间取决于浏览器的默认实现)。

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

为了避免浏览器启用默认缓存机制,应该在Respone Header加上Cache-Control字段。

1.3 缓存的状态

缓存的状态有:未过期(Fresh)、过期(Stale),

状态判断基于Age(此资源上一次从源服务器的正常Respone的时间,http状态为200或304)和当前的时间差额,再和Respone Header指定有效期比较,大于则过期,小于则未过期。

例,下面Cache-Control中的max-age指定此资源的过期时间为604800秒,一周时间。 如果 当前时间 减去 Tue, 22 Feb 2022 22:22:22 GMT 大于1周那么此资源的缓存过期。

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>
…

当客户端和服务端中间还有共享缓存时,共享缓存会额外返回其缓存此资源的时间(Age字段),如下方所示,那么此资源在客户端缓存的有效时间为: max-age 减去 age = 518400秒 。

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>
…

早期的HTTP/1.0中使用Expires Header 来直接明确资源的过期时间,但是此方式会被本地系统时间不准的问题导致未按照预期方式使用缓存;所以在HTTP/1.1 中的cache-Control直接通过max-age指定“差额时间”来避免。

如果Header中同时出现Expires和Cache-Control: max-age时,后者优先生效。

Expires: Tue, 28 Feb 2022 22:22:22 GMT

Cache-Control具体值参考:

  • max-age(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。

    • max-age>0表示在设置时间内请求直接从浏览器缓存中读取,使用强缓存
    • max-age<=0表示请求到服务器,服务器需要判断文件是否已更新,进而返回200还是304
  • no-cache:设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。

    • Cache-Control: no-cache, max-age=2000 表示在2000秒内使用强缓存,超过2000秒使用协商缓存
  • no-store:禁用缓存。

  • public:表明其他用户也可使用缓存,适用于公共缓存服务器的情况。如果没有指定public还是private,则默认为public。

  • private:表明只有特定用户才能使用缓存,适用于公共缓存服务器的情况。

  • s-maxage:适用于多用户使用的公共缓存服务器,比如CDN。比如,当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。也就是说max-age用于普通缓存,而s-maxage用于代理缓存。如果存在s-maxage,则会覆盖掉max-age和Expires header。

Pragma

Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,在Expires、Cache-Control三个中优先级最高。 他是HTTP/1.0时代产物,现在不推荐使用。

1.4 客户端如何命中缓存?

默认浏览器是以url作为唯一标识确定资源的唯一性来确定是否命中本地缓存,但是可以通过配置Vary Header来组合标识资源的唯一性,Vary的值设置为Accept-Language时: image.png
还可以设置为 User-Agent、Cookie,但要考虑设置后缓存是否能被最大来利用,否则意义不大。

image.png

1.5 缓存协商

下图为大部分情况下http cache进行处理的流程:

image.png 注意图中没有关于no-cache的判断部分,下面会提到。

当本地缓存状态为已过期时,并不会马上被删除。Http会根据关键信息和源服务器进行协商比对,来确认此资源在本地的缓存是否是最新的,这种机制就是缓存协商

缓存协商一般通过包含了If-Modified-Since或If-None-Match请求标头的条件来完成验证。

If-Modified-Since

下面的资源只能被缓存1小时,根据Date得知过期时间为23:22:22,之后此缓存将会过期。

Response HeaderHTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>
…

下次再请求此资源时,浏览器会以以下方式进行缓存协商:询问源服务器此资源是否在指定时候后被修改过。

Request HeaderGET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

当源服务经过比对,发现资源未被修改过会返回HTTP状态码值304,告知客户端继续使用本地缓存,而不是重复返回资源副本,此时此缓存有效期在最新Date的基础上继续延长max-age指定时间。

Response HeaderHTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

If-Modified-Since 存在一些短板,比如:1.源服务器为分布式架构,相互之间时间同步不准。 2.对于秒级的修改不敏感。

所以出现了Etag reponse header来替代解决这些问题。

ETag/If-None-Match

ETag值完全取决于服务端如何生成,可以是文件的hash值也可以是固定的算法来生成唯一的ID。

Response HeaderHTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600

<!doctype html>
…

如果资源缓存过期,客户端会发起如下的reuqest来进行缓存协商确认是否继续使用本地缓存。

Request HeaderGET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"

服务端返回304时则表示此资源可以继续使用本地缓存,如果是200时返回最新资源副本,表示资源进行过修改,同时客户端继续根据缓存规则进行本地缓存。

如果Etag和Last-Modified同时出现时,前者优先。但是后者不仅仅是用于缓存协商机制,比如作为网络爬虫程序调整爬取频率的基准。或是在文本管理界面提供文件最后修改时间的展示。 所以推荐Last-Modified和Etag同时设置。

强制协商

如果想资源能被重复使用,但是必须保证是最新的副本。 可以通过设置Cache-Control: no-cacheLast-Modified and ETag一起使用。 这样客户端将不管本地缓存的状态,而是每次都与服务端进行协商(如果返回Http 200将更新本地缓存再使用,304则继续使用本地缓存)

Request HeaderHTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>
…

因为Http/1.1之前许多实现无法处理no-cache指定,也可以使用如下标头,must-revalidate表示每次必须协商,max-age=0表示资源副本立即过期。 但是现在符合 HTTP/1.1 的服务器已经被广泛部署,没有理由使用它max-age=0must-revalidate组合——你应该只使用no-cache.

Cache-Control: max-age=0, must-revalidate

重载(reload)和强制重载(force reload)

浏览器提供了重载(刷新)和强制重载的操作选项。 此类操作将会默认在Request Header中添加Cache-Control: max-age=0 Cache-Control: no-cachePragma: no-cache参数,提示浏览器不实用缓存(即使在生效期间)直接从源服务获取最新资源。

这也是为什么我们在一个可能因为缓存导致问题的页面进行刷新能解决问题。

1.6 不使用缓存

因为资源的隐私性考虑想保证资源永远是最新的,可以将Cache-Control设置为no-store.

Cache-Control: no-store

注意:返回no-store 不会影响客户端已经进行了缓存的资源,换句话说如果资源已经被缓存到了客户端本地,再在respone中返回no-store,客户端还会使用缓存资源。
不过no-cache可以强制客户端在重用缓存资源时进行缓存协商。

no-store指令也可能不被某些过期的客户端所识别,推荐通过以下标头组合来达到客户端永远使用的是最新资源的目的。

Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

⚠️注意!一旦使用此策略将失去最开始提到的HTTP Cache带来的优势,包括浏览器的后退和向前的缓存。

1.7 缓存的存储范围

如果缓存是包含客户个人信息的,使用以下标头防止共享缓存生效。

Cache-Control: private

反之可以设置为public,使中间代理进行缓存,进一步减轻服务端压力和提升客户体验(当然前提是客户端到服务端之间存在响应的缓存代理服务)。

2. 缓存设置最佳实践

为了避免默认的启发式缓存,最好显式地为所有响应提供一个默认Cache-Control标头。

  • 保证资源是最新的,同时又利用http cache优势:

    通常的做法是使默认Cache-Control值包括no-cache,此外,如果服务实现了 cookie 或其他登录方式,并且内容是针对每个用户个性化的,private也必须提供,以防止与其他用户共享:

Cache-Control: no-cache, private

关于css、js的缓存问题解决,网上有看到通过修改资源文件名称或者在请求URL后新增query String,也可以解决问题(由于缓存根据它们的 URL 来区分资源,因此如果在更新资源时 URL 发生变化,缓存将不会再次被重用)。
但是经过自己的验证,如果Cache-Control:no-cache能解决问题,就不需要那么麻烦了。

而且请求URL后新增query String这种方式可能不会被缓存代理服务过滤掉导致更新缓存资源失败,参考

3. HTTP Cache设置方式

关于HTTP Cache标头设置方式主要是通过服务端开发人员进行配置,大致分为中间件提供和自行实现,对了Last-ModifiedETag标头大部分中间件自行会生成,无需自行配置。

中间件现有模块

  • Tomcat 自带的ExpiresFiler可以根据资源类型设置max-age。
  • Apache Http Server 提供的mod_headers模块,针对Header进行更细化的设置;还有mod_expires模块。
  • Nginx

自行实现

  • 传统项目可以在web.xml中添加filter,针对不同资源返回的repsone进行Cache-Control配置。
<filter>
    <filter-name>FilterCache</filter-name>
    <filter-class>com.yukuilab.cache.FilterCache</filter-class>
</filter>
<filter-mapping>
    <filter-name>FilterCache</filter-name>
    <url-pattern>*.js</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class FilterCache implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate"); //此处为不使用缓存,根据自身诉求自行替换
        chain.doFilter(request, response);
    }
    
    @Override
    public void destroy() {
    }
}
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />

<meta http-equiv="Pragma" content="no-cache" />

<meta http-equiv="Expires" content="0" />

参考:
HTTP缓存介绍-官方
一文搞懂http缓存
HTTP 缓存别再乱用了!推荐一个缓存设置的最佳姿势!
Strategies for Cache-Busting CSS
How to force browsers to reload cached CSS and JS files?