web缓存原理分析

为什么2月份会停更一个月的博客呢? … 过年是一个原因, 其次就是改bug, 改bug的过程感觉很恶心, 没有什么技术上的收获, 只有经验上的收获, 例如多自测几套数据, 多测测极限数据的问题, 而这些又没什么好写的, 也就一直没有更新博客. 上班真的很辛苦, 每天感觉挺累的, 书也好久没有看了, 今天恰逢没有新需求, 项目在提测之际来写下一遍转载的文章, 主要记录一下在各处搜索到的关于web缓存的一些事情.


这边文章其实写在3月22日之前, 刚参加完腾讯的面试觉得差距真是非常大, 很多内部实现原理之前根本没想学习, 现在发现这是不行的,
http://blog.csdn.net/c_kite/article/details/79646035
这是面试经历


各种类型的缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这样带来的好处有:缓解服务器端压力,提升性能(获取资源的耗时更短了)。对于网站来说,缓存是达到高性能的重要组成部分。缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。

1.数据库数据缓存
Web应用,特别是社交网络服务类型的应用,往往关系比较复杂,数据库表繁多,如果频繁进行数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。比如常用的缓存方案有memcached,redis等。

2.服务器端缓存
代理服务器缓存
代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。常见代理服务器缓存解决方案有Squid,Nginx,Apache等。

CDN缓存
CDN(Content delivery networks)缓存,也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器,浏览器和服务器之间的缓存机制,在这种架构下同样适用。

3.浏览器端缓存
浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果你浏览过程中,比如前进或后退,访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。

4.Web应用层缓存
应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据,页面,图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。

HTTP头信息控制缓存

通用首部字段

我们先来瞅一眼http1.1协议报文首部字段中与缓存相关的字段

1.通用首部字段

字段名称 说明
Cache-Control 控制缓存的行为
Pragma http1.0的旧社会遗留物, 值为”no-cache”时禁用缓存

2.请求首部字段

字段名称 说明
if-Match 比较ETag是否一致
if-None-Match 比较ETag是否不一致
if-Modified-Since 比较资源最后更新的时间是否一致
if-Unmodified-Since 比较资源最后更新的时间是否不一致

3.响应首部字段

字段名称 说明
ETag 资源的匹配信息

4. 实体首部字段

字段名称 说明
Expires http1.0的遗留物, 实体主体过期的时间
Last-Modified 资源最后一次修改的时间

http1.0 时代缓存字段详解

在 http1.0 时代,给客户端设定缓存方式可通过两个字段PragmaExpires来规范。虽然这两个字段早可抛弃,但http协议做了向下兼容,所以依然可以看到。

1.Pragma

Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存

当该字段值为no-cache的时候,会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。

2.Expires

有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires就是做这件事的首部字段。 Expires的值对应一个GMT(格林尼治时间),比如Mon, 22 Jul 2002 11:12:01 GMT来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。

如果Pragma头部和Expires头部同时存在,则起作用的会是Pragma,需要注意的是,响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,其定义的是资源“失效时刻”,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。


http1.1时代缓存字段详解

1.Cache-Control

针对上述的Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了 Cache-Control 来定义缓存过期时间。注意:若报文中同时出现了 ExpiresCache-Control,则以 Cache-Control 为准。

也就是说优先级从高到低分别是 Pragma -> Cache-Control -> Expires

Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。在RFC中规范了 Cache-Control 的格式为:

1
Cache-Control: cache-directive

作为请求首部时,cache-directive 的可选值有:

字段名称 说明
no-cache 告知代理服务器不直接使用缓存, 要求向原服务器发起请求
no-store 所有内容都不会被保存到缓存或Internet临时文件中
max-age=delta-seconds 告知服务器客户端希望接收一个存在时间(Age)不大于delta-seconds秒的资源
max-stale[=delta-seconds] 告知(代理)服务器客户端愿意接收一个超过缓存时间的资源, 若有定义delta-seconds则为delta-seconds秒, 若没有则为任意超过的时间
min-fresh=delta-seconds 告知(代理)服务器客户端希望接收一个在小于delta-seconds秒内被更新过的资源
no-transform 告知(代理)服务器客户端希望获取实体数据没有被转换(比如压缩)过的资源
only-if-cached 告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求
cache-extension 自定义扩展值,若服务器不识别该值将被忽略掉

Cache-Control: no-cache:这个很容易让人产生误解,使人误以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,
只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
Cache-Control: no-store:这个才是响应不被缓存的意思。

作为相应头部, cache-directive的可选值:

字段名称 说明
public 表明任何情况下都得缓存该资源(即使是需要http认证的资源)
Private[=”field-name”] 表明返回报文中全部或部分(若指定了field-name则为field-name的字段数据)仅开放给某些用户(服务器指定的share-user, 如代理服务器)做缓存使用, 其他用户则不能缓存这些数据
no-cache 不直接使用缓存, 要求向服务器发起(新鲜度校验)请求
no-store 所有内容都不会被保存到缓存或Internet临时文件中
no-transform 告知客户端缓存文件时不得对实体数据做任何改变
only-if-cached 告知(代理)服务器客户端希望获取缓存的内容(若有), 而不用向原服务器发去请求
must-revalidate 当前资源一定是向原服务器发去验证请求的, 若请求失败会返回504(而非代理服务器上的缓存
proxy-revalidate 与must-revalidate类似, 但仅能应用于共享缓存(如代理)
max-age=delta-seconds 告知客户端该资源在delta-seconds秒内是新鲜的, 无需向服务器发送请求
s-maxage=delta-seconds 同max-age, 但仅应用于共享缓存(如代理)
cache-extension 自定义扩展值. 若服务器不识别该值将被忽略掉

Cache-Control 允许自由组合可选值,例如:

1
Cache-Control: max-age=3600, must-revalidate

它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。

当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-agemin-freshmax-stale 一起搭配使用。

2.Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since要配合Cache-Control使用。

(1) Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。

(2) If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache

3.Etag/If-None-Match

Etag/If-None-Match也要配合Cache-Control使用。

(1)Etag: web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器觉得)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

(2)If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304

4.既生Last-Modified何生Etag?

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

(1). Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

(2). 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存

(3). 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-ModifiedETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

5.不太常用的两个http字段If-Unmodified-Since/If-Match

(1)If-Unmodified-Since: Last-Modified-value
告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。

当遇到下面情况时,If-Unmodified-Since 字段会被忽略:

  1. Last-Modified值对上了(资源在服务端没有新的修改);
  2. 服务端需返回2XX和412之外的状态码;
  3. 传来的指定日期不合法

(2)If-Match: ETag-value
告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。


浏览器缓存流程图

小结一下, 浏览器第一次请求

第一次流程图

浏览器第二次请求

第二次流程图


如何配置

1)通过代码的方式,在web服务器返回的响应中添加Expires和Cache-Control Header;

比如在JavaWeb里面,我们可以使用类似下面的代码设置强缓存:

1
2
3
4
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); // Expires:过时期限值
response.setHeader("Cache-Control", "public"); // Cache-Control来控制页面的缓存与否,public:浏览器和缓存服务器都可以缓存页面信息;
response.setHeader("Pragma", "Pragma"); // Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存

还可以通过类似下面的java代码设置不启用强缓存:

1
2
3
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//浏览器和缓存服务器都不应该缓存页面信息

2)通过配置web服务器的方式,让web服务器在响应资源的时候统一添加Expires和Cache-Control Header。

tomcat提供了一个ExpiresFilter专门用来配置强缓存

这里写图片描述

nginx和apache作为专业的web服务器,都有专门的配置文件,可以配置expires和cache-control,

Nginx服务器的配置方法为:

1
2
3
4
5
6
7
8
9
10
11
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
#过期时间为30天,#图片文件不怎么更新,过期可以设大一点,
expires 30d;
}
location ~ .*\.(js|css)$ {
#如果频繁更新,则可以设置得小一点。
expires 1d;
add_header Cache-Control max-age=86400;
etag on;
}

Apache服务器的配置方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Location ~ "\.(js|css|png|jpg|gif|bmp|html)$">
ExpiresActive On
ExpiresDefault "access plus 1 hours"
Header set Cache-Control max-age=3600
Header unset Pragma
</Location>
<Location ~ "\.(do|jsp|aspx|asp|php|json|action|ashx|axd|cgi)$">
Header set Cache-Control no-cache,no-store,max-age=0
Header unset Expires
Etag INode Mtime Size
</Location>

3.缓存配置的一些注意事项

  1. 只有get请求会被缓存,post请求不会

  2. Etag 在资源分布在多台机器上时,对于同一个资源,不同服务器生成的Etag可能不相同,此时就会导致304协议缓存失效,客户端还是直接从server取资源。可以自己修改服务器端etag的生成方式,根据资源内容生成同样的etag。需要注意的是分布式系统里多台机器间文件的last-modified必须保持一致,以免负载均衡到不同机器导致比对失败,Yahoo建议分布式系统尽量关闭掉Etag(每台机器生成的Etag都会不一样,因为除了 last-modified、文档节点也很难保持一致)


用户行为与缓存

行为缓存流程图


缓存的清除方法

由于在开发的时候不会专门去配置强缓存,而浏览器又默认会缓存图片,css和js等静态资源,所以开发环境下经常会因为强缓存导致资源没有及时更新而看不到最新的效果,解决这个问题的方法有很多,常用的有以下几种:

1)直接ctrl+f5,这个办法能解决页面直接引用的资源更新的问题;

2)使用ctrl+shift+delete清除缓存;

3)如果用的是chrome,可以F12在network那里把缓存给禁掉(这是个非常有效的方法):

例图

4)在开发阶段,给资源加上一个动态的参数,如css/index.css?v=0.0001,由于每次资源的修改都要更新引用的位置,同时修改参数的值,所以操作起来不是很方便,一般使用前端的构建工具来修改这个参数

4.1) 原生写法

1
2
3
4
5
6
7
8
9
function addVersion(asset){
asset.forEach(function(item,index) {
if(item.indexOf('.js') != -1) {
document.write('<script src="'+item+'?v='+ (new Date().getTime()) +'"><\/script>');
}else if(item.indexOf('.css') != -1){
document.write('<link rel="stylesheet" href="'+item+'?v='+(new Date().getTime())+'">');
}
});
}

4.2) 采用gulp插件

4.2.1) gulp-rev-append

这里写图片描述

4.2.2) gulp-rev和gulp-rev-collector也能实现同样的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 修改html和css文件,给静态文件打戳
gulp.task('stamp', function(){
gulp.src(['rev/*.json', dest.css + "**/*.css"]).
pipe(revCollector({
replaceReved: true
})).
// 修改为 ?v=stamp 形式
pipe(replace(/\-([0-9a-z]{8,})\.(png|jpg|gif|ico)/g, function(a, b, c){
return '.' + c + '?v=' + b;
})).
pipe(gulp.dest(dest.css));
gulp.src(['rev/*.json', src.html]).
pipe(revCollector({
replaceReved: true
})).
// 修改为 ?v=stamp 形式
pipe(replace(/\-([0-9a-z\-]{8,})\.(css|js)/g, function(a, b, c){
return '.' + c + '?v=' + b;
})).
pipe(gulp.dest(dest.html));
});

4.3)

如果资源引用的页面,被嵌入到了一个iframe里面,可以在iframe的区域右键单击重新加载该页面,以chrome为例:

这里写图片描述

4.4)

如果缓存问题出现在ajax请求中,最有效的解决办法就是ajax的请求地址追加随机数;

4.5)

还有一种情况就是动态设置iframe的src时,有可能也会因为缓存问题,导致看不到最新的效果,这时候在要设置的src后面添加随机数也能解决问题;


离线缓存

localstorage和sessionstorage太简单不说了

设置方法

1 . 在HTML5的html标签中添加一个 manifest=”XXX.appcache” 属性声明

1
2
<!DOCTYPE html>
<html manifest="list.appcache">

2 . XXX.appcache文件中定义需要缓存的文件清单(里面的资源文件的路径是相对于manifest的路径而言的)

1
2
3
4
5
6
7
8
CACHE MANIFEST
# VERSION 0.3
# 直接缓存的文件
CACHE:
# 需要在线访问的文件
NETWORK:
# 替代方案
FALLBACK:

CACHE MANIFEST –(必须) 此标题下列出的文件将在首次下载后进行缓存

1
2
3
4
5
../addDevice.html
../static/css/reset.css
../static/js/addDevice.js
../static/img/ms1.png
../static/img/clean-face.jpg

NETWORK—-(可选)

(1)通配符’*’表示不在CACHE MANIFEST清单里的文件,每次都要重新请求

*

(2)或者指定特定文件,比如login.asp不被离线存储,每次都要重新发起请求

login.asp

FALLBACK—-(可选) 断网时访问指定路径时的替换文件

如断网时访问/html5/ 目录下的所有资源文件,则用 “offline.html” 替代

/html5/ /offline.html

更新原理

更新了manifest文件,浏览器会自动的重新下载新的manifest文件并把manifest缓存列表中的所有文件重新请求一次(第二次刷新替换本地缓存为最新缓存),而不是单独请求某个特定修改过的资源文件,因为manifest是不知道哪个文件被修改过了的。

对于全局更新不必要担心,因为没有更新过的资源文件,请求依旧是304响应,只有真正更新过的资源文件才是服务器返回的才是200.

所以控制离线存储的更新,需要2个步骤,一是更新资源文件,二是更新manifest文件,只要修改manifest文件随意一处,浏览器就会感知manifest文件更新,而我们的资源文件名称通常是固定的,需要更新manifest文件怎么操作呢?一个比较好的方式是更新以# 开头的版本号注释,告诉浏览器这个manifest文件被更新过。

manifest资源是滞后静默更新的

这里写图片描述

第二次刷新界面之后,才能看到更新后的效果

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*code1,简单粗暴的*/
applicationCache.onupdateready = function(){
applicationCache.swapCache(); //强制替换缓存
location.reload(); //重新加载页面
};
/*code2,缓存公用方法*/
// var EventUtil = {
// addHandler: function(element, type, handler) {
// if (element.addEventListener) {
// element.addEventListener(type, handler, false);
// } else if (element.attachEvent) {
// element.attachEvent("on" + type, handler);
// } else {
// element["on" + type] = handler;
// }
// }
// };
// EventUtil.addHandler(applicationCache, "updateready", function() { //缓存更新并已下载,要在下次进入页面生效
// applicationCache.update(); //检查缓存manifest文件是否更新,ps:页面加载默认检查一次。
// applicationCache.swapCache(); //交换到新的缓存项中,交换了要下次进入页面才生效
// location.reload(); //重新载入页面
// });

applicationCache 提供了如下的事件:

Event handler Event handler event type
onchecking checking
onerror error
onnoupdate noupdate
ondownloading downloading
onprogress progress
onupdateready updateready
oncached cached
onobsolete obsolete

提供了如下的API:

void update();
// 更新, 但是这个方法适用于一些长期打开的页面,而不会有刷新动作,比如邮件系统,所以这个就比较适合做自动更新下载

void abort();
// 取消

void swapCache();
// 替换缓存内容 ,对于manifest文件的改变,通常是下一次的刷新才会触发下载更新,第二次刷新才会切换使用新的缓存文件,通过这个方法,可以强制将缓存替换

注意事项

站点中的其他页面即使没有设置manifest属性,请求的资源如果在缓存中也从缓存中访问

系统会自动缓存引用清单文件的 HTML 文件

如果manifest文件,或者内部列举的某一个文件不能正常下载,整个更新过程将视为失败,浏览器继续全部使用老的缓存

在manifest中使用的相对路径,相对参照物为manifest文件

站点离线存储的容量限制是5M

manifest文件中CACHE则与NETWORK,FALLBACK的位置顺序没有关系,如果是隐式声明需要在最前面

manifest中必须一一声明文件名,这很令人头痛

引用manifest的html必须与manifest文件同源,在同一个域下

除此之外,还增加了两大问题:

(1)PV UV的计算难题,由于当前页面被强制加入manifest,那么PV 和UV的统计,成了一个难题,因为请求不再是发送到服务器;

(2)缓存对于某个使用manifest的文件,其带有的参数可能是随机性的统计参数,如sid=123sss, sid=234fff ,尤其是比如商品详情的id字段等,这样每个页面都自动加入到manifest中,将会带来很大的存储开销,而且是毫无意义的;

所以伴随而来的,是如何在现有的体系架构下进行数据统计的难题,

对于第一个问题 常规方案是进入离线存储页面后自动发出ajax请求,以告知服务器统计PV UV;

对于第二个问题,是将GET请求方式改成POST方式。


转载:
[1]http://www.zhangxinxu.com/wordpress/2013/05/caching-tutorial-for-web-authors-and-webmasters/

[2]http://www.codeceo.com/article/http-cache-control.html

[3]http://www.cnblogs.com/Joans/p/3956490.html

[4]http://web.jobbole.com/86970/

[5]http://www.jianshu.com/p/1a9268594deb

[6]http://blog.techbeta.me/2016/02/http-cache/

[7]http://hahack.com/wiki/sundries-http-web.html

[8]http://www.jianshu.com/p/99dc1f8f62bf

[9]https://www.cnblogs.com/wangpenghui522/p/5498427.html

越来越多的平台(微信公众平台,新浪微博,简书,百度打赏等)支持打赏功能,付费阅读时代越来越近,特此增加了打赏功能,支持微信打赏和支付宝打赏。坚持原创技术分享,您的支持将鼓励我继续创作!