Http缓存
关于Http缓存这部分内容在网上查阅资料时,发现很多文章将其分为*强制缓存、协商缓存或者对比缓存*,但笔者在RFC文档中并没有找到相关词汇,所以本文并不会采用上述分类,而是以RFC文件及《Http权威指南》中的内容为准。除此之外还需要明确的一点:本文并不讨论【代理缓存】,只分析客户端本身进行缓存的情况。
RFC文档:https://datatracker.ietf.org/doc/html/rfc2616#section-13,中主要涉及到的Http缓存相关的内容如下:
缓存处理步骤
整个流程比较简单,可能需要解释的有两个名词
1.判断是否新鲜,也就是新鲜度检测,可以理解为检查缓存是否已经过期
2.服务器再验证,在确认了缓存已经过期的情况我们还需要到服务器去确认过期的缓存是否还有效,如果仍然有效的话此时我们需要将客户端的缓存重新生效,这个过程称之为再验证(revalidation)。
关于第二点小伙伴们可能会有疑问:
为什么确认缓存已经过期了还需要去服务端验证呢,缓存过期不应该直接请求服务器返回最新数据吗?
再验证的话多了一次验证过程不是增加了网络开销了吗?
第一个问题,答:
缓存过期并不意味着缓存中的数据跟服务器保存的数据不一致,例如服务器通过Cache-Control 告诉了客户端缓存有效期为两小时,但在接下来的两小时内服务器上的这份数据并没有任何写入操作,也就是说虽然客户端在检测的时候数据已经过期了,但是客户端此时缓存的数据仍然跟服务器保存的最新数据是一致的,此时也就没必要让服务重新发送一份客户端已经缓存的数据,我们只需要服务器通过某种机制告诉客户端缓存是可用的即可。
第二个问题,答:
缓存的再校验跟向服务器请求最新数据往往会被合并成一个请求。我们可以在发送请求时附加一些用于验证的头信息,比如我们可以给缓存的实体打上一个标签,每次向服务器发送请求时携带上这个标签,当进行再验证时服务器校验客户端当前记录的数据标签是否跟自身保存的一致,如果一致告诉服务器缓存是可用的(304响应码),如果不一致则返回最新数据及最新标签。如下:
通过前文所述相信大家对Http的缓存机制有了一个大概的了解,那么接下来我们就开始分析其中的细节:如何完成新鲜度检测及再验证?
新鲜度检测
所谓新鲜度检测实际就是检测文档是否过期,服务器可以通过Cache-Control首部和 Expires首部指定返回数据的有效期,Expires是HTTP1.0的规范,使用的是决定时间,如下:
Expires: Wed, 21 Oct 2022 07:28:00 GMT
Cache-Control是HTTP1.1的规范,搭配max-age并使用相对时间,如下:
Cache-Control: max-age=30(单位为秒)
只要在有效期内,客户端即可认为此时的缓存数据是新鲜的,无须向服务器发送请求
再验证
如果客户端检测到此时缓存已经过期,那么需要向服务器发起再验证,一个具有再验证功能的请求跟普通的请求唯一的区别在于请求头中多了一些用于校验的字段,如下:
参考: https://datatracker.ietf.org/doc/html/rfc7232
字段名 |
描述 | 备注 |
If-Modified-Since |
如果从指定日期之后数据**【被修改】过了则验证失败,需要向服务器发送请求获取最新数据,如果验证成功,服务端返回「304(Not Modified)」** |
通过日期校验,通常用于缓存再校验,一般会结合响应头中的Last-Modified使用 |
If-None-Match |
如果缓存中数据的标签跟服务器数据的标签不匹配则验证失败,需要向服务器发送请求获取最新数据,与Etag 服务器响应首部配合使用,如果验证成功,服务端返回**「304(Not Modified)」** |
通过唯一标识进行校验,通常用于缓存再校验 |
1.If-Modified-Since再验证工作过程如下
客户端在第一次缓存时同时也记录了服务器返回的Last-Modified,再后续发现缓存过期时会向服务器发送一个再验证请求,在请求头中添加一个If-Modified-Since字段,其值为Last-Modified的值,服务器在收到此请求后,先判断在指定时间后数据是否发生了变化,如果没有变化则返回**「304(Not Modified)」**,否则返回200状态码及最新数据。
2.If-None-Match:实体标签再验证
整个校验过程跟If-Modified-Since是一致的,唯一的区别在于If-Modified-Since校验的是日志,而If-None-Match校验的是数据对应的唯一标签。如果 缓存数据中同时有 Etag 和 Last-Modified 字段的时候, Etag 的优先级更高,也就是先会判断 Etag 是否变化了,如果 Etag 没有变化,然后再看 Last-Modified。
这里需要说明一下,除了上面提到的两个头部字段外,Http中还定义了一些其它的带有校验含义的header,如下:
字段名 |
描述 |
备注 |
If-Match |
与Etag 服务器响应首部配合使用,校验失败返回**「412(Precondition Failed)」** |
并不用于缓存相关操作,而是用于避免错误的更新操作(PUT、POST、DELETE),只有在满足条件的情况下才允许更新,通常用于多人协作更新同一份数据时 |
If-Unmodified-Since | 如果从指定日期之后数据**【未被修改】则验证成功。验证失败时服务端需要返回「412(Precondition Failed)」** |
跟If-Match一样能避免错误的更新操作,不同的是If-Match比较的是标签而If-Unmodified-Since比较的是日期。另外在进行部分文件的传输时,获取文件的其余部分之前,要确保文件未发生变化,此时这个首部是非常有用的。例如在端点续传的场景下,需要保证服务端已经传送到客户端的资源没有发生变化。 |
If-Range |
支持对不完整文档的缓存,会搭配服务器响应中的Last-Modified或者ETag使用,验证失败时服务端需要返回**「412(Precondition Failed)」** |
主要用于范围请求或断点续传 |
读者朋友们需要注意的是,虽然If-Match、If-Unmodified-Since看起来是If-None-Match跟If-Modified-Since的反义词,但在HTPP协议中定义的语义是完全不一样的。具体可以参考:https://datatracker.ietf.org/doc/html/rfc7232#page-17