3000字讲透HTTP缓存

本文详细解析HTTP缓存的存储策略、过期策略和对比(协商)策略,涵盖Expires、Cache-Control、Last-Modified、ETag等关键字段,帮助理解浏览器如何处理HTTP缓存。

概述

本文从 HTTP缓存策略 为入口,讲解HTTP缓存在浏览器的应用。

文章按 强缓存 、 协商缓存 和 启发式缓存 三个类别,进行深入剖析。

HTTP缓存策略

HTTP缓存分为三大策略:

  • 存储策略
  • 过期策略
  • 对比策略(也叫协商策略)

存储策略

存储策略,用于决定HTTP响应内容,是否可以缓存到客户端。

Cache-Control 头的 max-age 、 no-cache 、 no-store 、 public 、 private 、 s-maxage ,使用存储策略,来指明资源文件是否可以被缓存。

过期策略

过期策略,用于决定客户端是否可以直接从本地缓存中读取文件数据,而不需要发起HTTP请求。

响应头包含 Cache-Control: public 的文件,虽然会被缓存,但不能明确当前文件是否在有效期内,所以需要其他字段做好“过期策略”。

强缓存的 Expires 字段,就是用 “过期策略” 定义缓存文件的有效期。借此,浏览器可判断是否需要发起HTTP请求。

响应头包含 Cache-Control: max-age=<seconds> 的文件,则包含存储策略和过期策略。

具体的策略应用,可以详细查阅下文的 Cache-Control 章节。

对比策略

将本地缓存文件的数据标识,发送到服务端进行验证,判断文件是否有效。这种策略,就叫对比策略,也叫协商策略。

对比策略,用于协商缓存场景,对应的字段是:

  • Last-Modified 和 If-Modified-Since
  • ETag 和 If-None-Match

例如:

ETag 用于存储缓存文件的哈希值。

浏览器需要判断当前缓存文件是否有效时,需要将 ETag 的值放入请求头 If-None-Match 字段,发送到服务端。

服务端接收到请求后,对比 If-None-Match 中的值与最新文件的值是否一致,来决定是否使用缓存。

当两个值一致时,则返回HTTP状态码304,告知浏览器,可使用本地缓存文件。

当两个值不一致时,则返回HTTP状态码200,并携带最新的文件返回给浏览器。

具体的策略应用,可以详细查阅下文的 协商缓存 章节。

小结

强缓存

强缓存通过字段 Expires 和 Cache-Control 来控制本地缓存文件的有效期。

如果本地缓存有效,则浏览器不会发起HTTP请求。

在浏览器控制台 NetWork 中的体现为:

200 OK (from disk cache) 或者  200 OK (from memory cache)

释义

200 OK (from disk cache)
200 OK (from memory cache)

强缓存的字段

字段协议版本缓存类型响应头请求头
ExpiresHTTP1.0强缓存:o:️:x:
Cache-ControlHTTP1.1强缓存:o:️:o:️

HTTP1.1字段 优先级比 HTTP1.0字段高。

Expires

Expires 表示缓存的过期时间,时间代表的是 服务端的时间 。

如果本地时间小于 Expires 的时间,则在有效期内。浏览器会直接读取缓存,不会发起HTTP请求。

Expires: Sun, 14 Jun 2020 02:50:57 GMT

缺点

Expires 受限于本地时间,如果本地时间修改,则可能会导致缓存失效。

Cache-Control

Cache-Control 比较特殊,可以在 响应头 和 请求头 中使用。它通过提供不同的值,来定义缓存策略。

Cache-Control 是所有缓存定义字段中,优先级最高的。

Cache-Control 字段取值含义存储策略过期策略响应头请求头
max-age缓存资源, 但是在指定时间(单位为秒)后缓存过期:o:️:o:️:o:️:o:️
no-cache相当于 max-age:0,must-revalidate 即资源被缓存, 但是缓存立刻过期, 同时下次访问时强制验证资源有效性:o:️:o:️:o:️:o:️
no-store请求和响应都不缓存:o:️:x::o:️:o:️
public资源将被客户端和代理服务器缓存:o:️:x::o:️:x:
private资源仅被客户端缓存, 代理服务器不缓存:o:️:x::o:️:x:
s-maxage依赖public设置, 覆盖max-age, 且只在代理服务器上有效:o:️:o:️:o:️:x:
must-revalidation / proxy-revalidation如果缓存失效, 强制重新向服务器(或代理)发起验证(因为max-stale等字段可能改变缓存的失效时间):x::o:️:o:️:x:
max-stale指定时间内, 即使缓存过时, 资源依然有效:x::o:️:x::o:️
min-fresh缓存的资源至少要保持指定时间的新鲜期:x::o:️:x::o:️
only-if-cached仅仅返回已经缓存的资源, 不访问网络, 若无缓存则返回504:x::x::x::o:️
no-transform强制要求代理服务器不要对资源进行转换, 禁止代理服务器对 Content-Encoding ,  Content-Range ,  Content-Type 字段的修改(因此代理的gzip压缩将不被允许):x::x::o:️:o:️

释义

  • Cache-Control:max-age=31536000 距离请求发起的时间 + 31536000 秒之后,才会过期
  • Cache-Control: must-revalidate 缓存过期的任何情况下,都必须发起请求重新验证
  • Cache-Control: s-maxage=60 同max-age作用一样,距离请求发起的时间 + 60 秒之后,才会过期。
    只在代理服务器中生效(比如CDN缓存), s-maxage优先级高于max-age,只对 public 缓存有效 。设置了 s-maxage,没设置 public,代理服务器也可以缓存这个资源。
  • Cache-Control: no-store 所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。
  • Cache-Control: no-cache 是否使用本地缓存都需要经过协商缓存来验证决定。使用 Etag 或者 Last-Modified 字段来控制缓存。
  • Cache-Control:max-age=31536000,max-stale=60 距离请求发起的时间 + 31536000 秒 + 60 秒之后,缓存才会失效。
    max-stale 表示最大容忍的过期时间,单位是秒。
  • Cache-Control:max-age=31536000, min-fresh=60 距离请求发起的时间 + 31536000 秒 - 60 秒之后,缓存才会失效。
    min-fresh 表示最小要留有N秒的新鲜度,单位是秒。

当max-age 与 max-stale 和 min-fresh 同时使用时, 它们的设置相互之间独立生效, 最为保守的缓存策略总是有效。

即哪个过期时间最早,就在这个过期时间后,发起资源请求,重新向服务端做验证。

协商缓存

当浏览器对某个资源的请求,没有命中强缓存,并在本地查找到缓存文件,则会发一个请求到服务器,验证本地缓存是否有效。

如果本地缓存文件有效,服务端响应请求,返回HTTP状态为: 304(Not Modified) , 不带消息主体。

如果本地缓存文件过期,服务端响应请求,返回HTTP状态为: 200 ,并携带资源实体数据。

协商缓存的字段

协商缓存字段分为两种:

  • Last-Modified 和 If-Modified-Since
  • ETag 和 If-None-Match
字段Header类型协议版本缓存类型
Last-ModifiedResponse(响应头)HTTP1.0协商缓存
If-Modified-SinceRequest(请求头)HTTP1.0协商缓存
ETagResponse(响应头)HTTP1.1协商缓存
If-None-MatchResquest(请求头)HTTP1.1协商缓存

HTTP1.1字段 优先级比 HTTP1.0字段高。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地文件的最后修改日期(精确到秒级)。

当浏览器发起资源请求时,会将文件的 Last-Modified 值,放入 If-Modified-Since 中,发送给服务端,询问该文件在该日期后,是否有更新。

如果在本地打开并修改缓存文件,则会导致Last-Modified日期被修改。

以下是例子:

  • 响应头
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
  • 请求头
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

缺点

If-Modified-Since

注意点

  • Last-Modified 是服务端响应头 Response Headers 中的字段。
  • If-Modified-Since 是客户端请求头 Request Headers 中的字段。
  • If-Modified-Since  只可以用在  GET  或  HEAD 请求中 
  • 当与  If-None-Match 一同出现时, If-None-Match 的优先级更高。 If-Modified-Since 会被忽略掉,除非服务器不支持  If-None-Match 。

ETag 和 If-None-Match

ETag 像文件的指纹一样,每次内容一更改, ETag 值都会发生变化。

当浏览器发起资源请求时,会将上一次文件的 ETag 值,放入 If-None-Match 中,发送给服务端,询问该文件是否有更新。

ETag 值之间的比较采用的是 弱比较算法 ,即两个文件除了每个字节都相同外,内容一致也可以认为是相同的。例如,如果两个页面仅仅在页脚的生成时间有所不同,就可以认为二者是相同的。

举个例子:

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"  
ETag: W/"0815"

其中, 'W/' (大小写敏感) 表示使用 弱验证器 。

注意点

ETag 是服务端响应头 Response Headers 中的字段。

If-None-Match 是客户端请求头 Request Headers 中的字段。

避免“空中碰撞”与 HTTP状态码412

空中碰撞,是指同时有多个人修改同个文件,产生竞态。

如果服务端接收每个人的保存请求,则会出现相互覆盖的状态。

所以,需要依靠ETag值,在保存前,做一些校验,以避免这种情况。

当需要修改或上传文件时,包含有 If-Match 头( ETag 值)的 POST 请求,会发送给服务端。

If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

如果服务端的文件哈希值与 If-Match 头的值不相等,则证明文件已经修改过。

这种情况下,服务端可以返回 HTTP状态码412 Precondition Failed (先决条件失败)表示客户端错误,拒绝处理请求。

启发式缓存

在资源请求的响应头中没有出现 Expires ,  Cache-Control: max-age , 或  Cache-Control:s-maxage 字段, 并且设置了 Last-Modified , 那么浏览器默认会采用一个 启发式的算法 。

启发式缓存的算法

取响应头的 Date值 - Last-Modified值 的结果的10%作为缓存时间。

详细可查看Caching in HTTP 中的介绍,笔者截取部分原文如下:

If none of Expires, Cache-Control: max-age, or Cache-Control: s- maxage (see section 14.9.3) appears in the response, and the response does not include other restrictions on caching, the cache MAY compute a freshness lifetime using a heuristic. The cache MUST attach Warning 113 to any response whose age is more than 24 hours if such warning has not already been added.

Also, if the response does have a Last-Modified time, the heuristic expiration value SHOULD be no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.

The calculation to determine if a response has expired is quite simple:

response_is_fresh = (freshness_lifetime > current_age)

巩固练习题

怎么让浏览器不缓存静态资源

Cache-Control: no-cache, no-store, must-revalidate
<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>
  • 部分浏览器支持在HTML中禁用缓存
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>

相同作用的请求头

  • Cache-Control: no-cache 与 Cache-Control: max-age=0
    max-age=0 表示该资源已在0秒后过期,需要使用协商策略。因此,和 Cache-Control: no-cache 一样。

参考

 

### 缓存设计模式详解 #### 1. **缓存旁路模式 (Cache-Aside Pattern)** 缓存旁路是一种常见的缓存管理策略,适用于大多数场景。其核心思想是将缓存作为独立组件处理,应用程序负责维护缓存的一致性和生命周期。 - 当读取数据时,程序优先尝试从缓存中获取数据;如果没有命中,则从数据库中查询并将其写入缓存[^3]。 - 更新操作时,先更新数据库中的记录,随后使对应的缓存条目失效或主动刷新缓存内容[^3]。 实现方式如下: ```python def get_data(key): data = cache.get(key) # 尝试从缓存中获取数据 if not data: data = db.query(key) # 如果未命中则从数据库中查询 if data: cache.set(key, data) # 查询成功后回填到缓存 return data def update_data(key, value): db.update(key, value) # 首先更新数据库 cache.delete(key) # 主动删除对应缓存项 ``` --- #### 2. **读模式 (Read-Through Pattern)** 在这种模式下,缓存和持久化存储被紧密集成在一起。当客户端发起读请求时,如果缓存中没有目标数据,则由缓存层自动从底层数据库加载该数据,并将其填充至缓存中后再返回给调用方[^4]。 此方法减少了应用逻辑复杂度,因为所有的缓存管理和同步都交给了缓存框架完成。然而,它可能带来性能开销以及更高的耦合性。 代码示例: ```java public Object read(String key) { Object result = cache.readIfPresent(key); // 查看缓存是否有值 if (result == null) { // 若无,则去DB查找 result = database.read(key); if (result != null) { cache.write(key, result); // 加载完成后写入缓存 } } return result; } ``` --- #### 3. **写模式 (Write-Through Pattern)** 类似于 Read-Through 的设计理念,在 Write-Through 中每次写入都会同时作用于缓存和后台数据库。这意味着任何修改都需要等待两个地方均确认完毕才能结束事务。 虽然这种方式能够提供更强的数据一致保障,但由于双重写入增加了延迟时间,因此不适合高频写的环境。 伪代码展示: ```csharp void write(string key, object value){ bool successInCache = false; try{ cache.put(key,value); // 向缓存提交更改 successInCache=true; }catch(Exception e){ /* handle exception */ } if(successInCache || !isCriticalOperation()){ backendStorage.save(key,value); // 接下来保存到永久储存介质里 } } ``` --- #### 4. **异步写入模式 (Write-Behind Caching)** 为了提高吞吐量,Write-Behind 不会在立即执行实际的 DB 操作而是通过队列机制延后处理这些变更动作直到合适时机才触发真正的物理存储过程[^4]。这样做的好处是可以显著降低 IO 负担从而提升整体效率。 注意的是这种做法可能会引入短暂的时间窗口期间内造成不一致现象所以需谨慎选用适合场合比如对于那些容忍一定程度偏差的应用来说比较理想。 Python 实现片段: ```python from queue import Queue write_queue = Queue() def async_write(key, value): global write_queue write_queue.put((key, value)) # 放置进消息队列 def worker(): while True: item = write_queue.get() # 获取待处理的任务 key, value = item db.update(key, value) # 执行真实的db更新 cache.set(key, value) # 并同步更新缓存 ``` --- #### 5. **双层或多级缓存架构** 考虑到单一层次的缓存可能存在局限性(如容量不足、速度不够快等问题),采用分层结构成为一种有效解决方案。通常情况下我们会设置一层快速响应的小型内存缓存配合较大规模但相对较慢磁盘文件系统或者其他远程服务共同工作形成所谓的两级甚至更多级别体系[^1]。 例如 Redis + Memcached 或者 Local In-Memory Hash Map + Distributed Cache System 组成组合形式来满足不同需求下的最佳平衡点。 Java 层面简单例子: ```java class TwoLevelCache<K,V> implements Cache<K,V>{ private final Cache<K,V> levelOne=new HashMap<>(); private final Cache<K,V> levelTwo= new RemoteDistributedCache(); @Override public V get(K k){ V v=this.levelOne.get(k); if(v==null){ synchronized(this){ v=this.levelOne.get(k); if(v==null){ v=this.levelTwo.get(k); this.levelOne.put(k,v); } } } return v; } ... } ``` --- #### 总结说明 以上介绍了几种典型的缓存设计方案及其具体实践手段,每种都有各自适用范围及优缺点需要依据实际情况灵活运用。无论是简单的旁路还是复杂的多层次混合部署都要围绕业务特点展开深入分析评估之后再做决定。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值