原文网址:http://jinnianshilongnian.iteye.com/blog/2319573
简介
最近遇到很多人来咨询我关于浏览器缓存的一些问题,而这些问题都是类似的,因此总结本文来解答以后遇到类似问题的朋友。
因本文主要以浏览器缓存场景介绍,所以非浏览器场景下的一些用法本文不会介绍,而且本文以chrome为测试浏览器。
浏览器缓存是指当我们使用浏览器访问一些网站页面或者http服务时,根据服务端返回的缓存设置响应头将响应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务端验证内容是否过期即可。这样的好处可以减少浏览器和服务端之间来回传输的数据量,节省带宽提升性能。
首先看个例子;当我们第一次访问http://item.jd.com/1856588.html时将得到如下响应头:
然后接着按F5刷新页面,将得到如下响应头
第二次返回的相应状态码为304,表示服务端文档没有修过过,浏览器缓存的内容还是最新的。
接下来我们看下如何在Java应用层控制浏览器缓存。
示例
Last-Modified
如下是我们的spring mvc缓存测试代码:
@RequestMapping("/cache")
public ResponseEntity<String> cache(
HttpServletRequest request,
//为了方便测试,此处传入文档最后修改时间
@RequestParam("millis") long lastModifiedMillis,
//浏览器验证文档内容是否修改时传入的Last-Modified
@RequestHeader (value = "If-Modified-Since", required = false) Date ifModifiedSince) {
//当前系统时间
long now = System.currentTimeMillis();
//文档可以在浏览器端/proxy上缓存多久
long maxAge = 20;
//判断内容是否修改了,此处使用等值判断
if(ifModifiedSince != null && ifModifiedSince.getTime() == lastModifiedMillis) {
return new ResponseEntity<String>(HttpStatus.NOT_MODIFIED);
}
DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
String body = "<a href=''>点击访问当前链接</a>";
MultiValueMap<String, String> headers = new HttpHeaders();
//文档修改时间
headers.add("Last-Modified", gmtDateFormat.format(new Date(lastModifiedMillis)));
//当前系统时间
headers.add("Date", gmtDateFormat.format(new Date(now)));
//过期时间 http 1.0支持
headers.add("Expires", gmtDateFormat.format(new Date(now + maxAge)));
//文档生存时间 http 1.1支持
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<String>(body, headers, HttpStatus.OK);
}
为了方便测试,测试时将文档的修改时间通过millis参数传入,实际应用时可以使用如商品的最后修改时间等替代。
首次访问
首次访问http://localhost:9080/cache?millis=1471349916709,将得到如下响应头:
响应状态码200表示请求内容成功,另外有如下几个缓存控制参数:
Last-Modified:表示文档的最后修改时间,当去服务器验证时会拿这个时间去;
Expires:http/1.0规范定义,表示文档在浏览器中的过期时间,当缓存的内容超过这个时间则需要重新去服务器获取最新的内容;
Cache-Control:http/1.1规范定义,表示浏览器缓存控制,max-age=20表示文档可以在浏览器中缓存20秒。
根据规范定义Cache-Control优先级高于Expires;实际使用时可以两个都用,或仅使用Cache-Control就可以了(比如京东的活动页sale.jd.com)。一般情况下Expires=当前系统时间(Date) + 缓存时间(Cache-Control: max-age)。大家可以在如上测试代码进行两者单独测试,缓存都是可行的。
F5刷新
接着按F5刷新当前页面,将看到浏览器发送如下请求头:
此处发送时有一个If-Modified-Since请求头,其值是上次请求响应中的Last-Modified,即浏览器会拿这个时间去服务端验证内容是否发生了变更。接着收到如下响应信息:
响应状态码为304,表示服务端告诉浏览器说“浏览器你缓存的内容没有变化,直接使用缓存内容展示吧”。
注:在测试时要过一段时间更改下参数millis来表示内容修改了,要不然会一直看到304响应。
Ctrl+F5强制刷新
如果你想强制从服务端获取最新的内容,可以按Ctrl+F5:
浏览器在请求时不会带上If-Modified-Since,并带上Cache-Control:no-cache和Pragma:no-cache,这是为了告诉服务端说我请给我一份最新的内容。
from cache
当我们按F5刷新、Ctrl+F5强制刷新、地址栏输入地址刷新时都会去服务端验证内容是否发生了变更。那什么情况才不去服务端验证呢?即有些朋友还会发现有一些“from cache”的情况,这是什么情况下发生的呢?
从A页面跳转到A页面或者从A页面跳转到B页面时:
大家可以通过如上方式模拟,即从A页面跳转到A页面也是情况1。此时如果内容还在缓存时间之内,直接从浏览器获取的内容,而不去服务端验证。
访问页面http://item.jd.com/11056556.html,然后点击面包屑中的HTTP权威指南时会跳转到当前页面,此时看到如下结果,页面及页面异步加载的一些js、css、图片都from cache了。
还有如通过浏览器历史记录进行前进后退时也会走from cache。本文是基于chrome 52.0.2743.116 m版本测试,不同浏览器行为可能存在差异。
Age
一般用于代理层(如CDN),大家在访问京东一些页面时会发现有一个Age响应头,然后强制刷新(Ctrl+F5)后会发现其不断的变化;其表示此内容在代理层从缓存到现在经过了多长时间了,即在代理层缓存了多长时间了。
Vary
一般用于代理层(如CDN),用于代理层和浏览器协商什么情况下使用哪个版本的缓存内容(比如压缩版和非压缩版),即什么情况下后续请求才能使用代理层缓存的该版本内容,比如如下响应是告知浏览器Content-Encoding:gzip,即缓存代理层缓存了gzip版本的内容;那么后续的请求在请求时Accept-Encoding头部中包含gzip时才能使用改代理层缓存。
Via
一般用于代理层(如CDN),表示访问到最终内容经过了哪些代理层,用的什么协议,代理层是否缓存命中等等;通过它可以进行一些故障诊断。
ETag
@RequestMapping("/cache/etag")
public ResponseEntity<String> cache(
HttpServletRequest request,
HttpServletResponse response,
//浏览器验证文档内容的实体 If-None-Match
@RequestHeader (value = "If-None-Match", required = false) String ifNoneMatch) {
//当前系统时间
long now = System.currentTimeMillis();
//文档可以在浏览器端/proxy上缓存多久
long maxAge = 10;
String body = "<a href=''>点击访问当前链接</a>";
//弱实体
String etag = "W/\"" + md5(body) + "\"";
if(StringUtils.equals(ifNoneMatch, etag)) {
return new ResponseEntity<String>(HttpStatus.NOT_MODIFIED);
}
DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
MultiValueMap<String, String> headers = new HttpHeaders();
//ETag http 1.1支持
headers.add("ETag", etag);
//当前系统时间
headers.add("Date", gmtDateFormat.format(new Date(now)));
//文档生存时间 http 1.1支持
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<String>(body, headers, HttpStatus.OK);
}
其中ETag用于发送到服务端进行内容变更验证的,而Catch-Control是用于控制缓存时间的(浏览器、代理层等)。此处我们使用了弱实体W\”343sda”,弱实体(”343sda”)只要内容语义没变即可,比如内容的gzip版和非gzip版可以使用弱实体验证;而强实体指字节必须完全一致(gzip和非gzip情况是不一样的),因此建议首先选择使用弱实体。nginx在生成etag时使用的算法是Last-Modified + Content-Length计算的:
ngx_sprintf(etag->value.data,"\"%xT-%xO\"",
r->headers_out.last_modified_time,
r->headers_out.content_length_n)
到此简单的基于文档修改时间和过期时间的缓存控制就介绍完了,在内容型响应我们大多数根据内容的修改时间来进行缓存控制,ETag根据实际需求而定(比如)。另外还可以使用html Meta标签控制浏览器缓存,但是对代理层缓存无效,因此不建议使用。
总结
1、服务端响应的Last-Modified会在下次请求时以If-Modified-Since请求头带到服务端进行文档是否修改的验证,如果没有修改则返回304,浏览器可以直接使用缓存内容;
2、Cache-Control:max-age和Expires用于决定浏览器端内容缓存多久,即多久过期,过期后则删除缓存重新从服务端获取最新的;另外可以用于from cache场景;
3、http/1.1规范定义的Cache-Control优先级高于http/1.0规范定义的Expires;
4、一般情况下Expires=当前系统时间 + 缓存时间(Cache-Control:max-age);
5、http/1.1规范定义了ETag来通过文档摘要的方式控制。
Last-Modified与ETag同时使用时,浏览器在验证时会同时发送If-Modified-Since和If-None-Match,按照http/1.1规范,如果同时使用If-Modified-Since和If-None-Match则服务端必须两个都验证通过后才能返回304;且nginx就是这样做的。因此实际使用时应该根据实际情况选择。还有If-Match和If-Unmodified-Since本文就不介绍了。
接下来我们看下如何使用nginx进行缓存控制。
nginx缓存设置
nginx提供了expires、etag、if-modified-since指令来进行浏览器缓存控制。
expires
假设我们使用nginx作为静态资源服务器,此时可以使用expires进行缓存控制。
location /img {
alias /export/img/;
expires 1d;
}
当我们访问静态资源时,如http://192.168.61.129/img/1.jpg,将得到类似如下的响应头:
对于静态资源会自动添加ETag,可以通过添加“etag off”指令禁止生成ETag。如果是静态文件Last-Modified是文件的最后修改时间;Expires是根据当前服务端系统时间算出来的。如上nginx配置的计算逻辑(实际计算逻辑比这个多,具体参考官方文档):
if (expires == NGX_HTTP_EXPIRES_ACCESS ||r->headers_out.last_modified_time == -1) {
max_age = expires_time;
expires_time += now;
}
if-modified-since
此指令用于表示nginx如何拿服务端的Last-Modified和浏览器端的If-Modified-Since时间进行比较,默认“if_modified_since exact”表示精确匹配,也可以使用“if_modified_sincebefore”表示只要文件的上次修改时间早于或等于浏览器短的If-Modified-Since时间,就返回304。
nginx proxy expires
使用nginx作为反向代理时,请求会先进入nginx,然后nginx将请求转发给后端应用。如下图所示:
首先配置upstream:
upstream backend_tomcat {
server 192.168.61.1:9080 max_fails=10 fail_timeout=10s weight=5;
}
接着配置location:
location = /cache {
proxy_pass http://backend_tomcat/cache$is_args$args;
}
接下来我们可以通过如http://192.168.61.129/cache?millis=1471349916709访问nginx,nginx会将请求转发给后端java应用。也就是说nginx只是做了相关的转发(负载均衡),并没有对请求和响应做什么处理。
假设对后端返回的过期时间需要调整,可以添加expires指令到location:
location = /cache {
proxy_pass http://backend_tomcat/cache$is_args$args;
expires 5s;
}
然后再请求相关的URL,将得到如下响应:
过期时间相关的响应头被expires指令更改了,但是Last-Modified是没有变的。
即使我们更改了缓存过期头,但nginx本身没有对这些内容做缓存,每次请求还是要到后端验证的,假设在过期时间内,这些验证在nginx这一层验证就可以了,不需要到后端验证,这样可以减少后端的很大压力。即整体流程是:
1、浏览器发起请求,首先到nginx,nginx根据url在nginx本地查找是否有文档缓存;
2、nginx没有找到本地缓存,则去后端获取最新的文档,并放入到nginx本地缓存中;返回200状态码和最新的文档给浏览器;
3、nginx找到本地缓存了,首先验证文档是否过期(Cache-Control:max-age=5),如果过期则去后端获取最新的文档,并放入nginx本地缓存中,返回200状态码和最新的文档给浏览器;如果文档没有过期,如果If-Modified-Since与缓存文档的Last-Modified匹配,则返回300状态码给浏览器,否则返回200状态码和最新的文档给浏览器。
即内容不需要动态(计算、渲染等)速度更快,内容越接近于用户速度越快。像apache traffic server、squid、varnish、nginx等技术都可以来进行内容缓存。还有CDN就是用来加速用户访问的:
即用户首先访问到全国各地的CDN节点(使用如ATS、Squid实现),如果CDN没命中,会回源到中央nginx集群,该集群如果没有命中缓存(该集群的缓存不是必须的,要根据实际命中情况等决定),最后回源到后端应用集群。
像我们商品详情页的一些服务就大量使用了nginx缓存减少回源到后端的请求量,从而提升访问速度。可以参考《构建需求响应式亿级商品详情页》、《京东商品详情页服务闭环实践》和《应用多级缓存模式支撑海量读服务》。
nginx代理层缓存
http模块配置:
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 512 4k;
proxy_busy_buffers_size 64k;
proxy_cache_path /export/cache/proxy_cachelevels=1:2 keys_zone=cache:512m inactive=5m max_size=8g use_temp_path=off;
#proxy timeout
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
其中红色部分是proxy_cache_path指令相关配置:
levels=1:2 :表示创建两级目录结构,比如/export/cache/proxy_cache/7/3c/,将所有文件放在一级目录结构中如果文件量很大会导致访问文件慢;
keys_zone=cache:512m :设置存储所有缓存key和相关信息的共享内存区,1M大约能存储8000个key;
inactive=5m :inactive指定被缓存的内容多久不被访问将从缓存中移除,以保证内容的新鲜;默认10分钟;
max_size=8g :最大缓存阀值,“cachemanager”进程会监控最大缓存大小,当缓存达到该阀值,该进程将从缓存中移除最近最少使用的内容;
use_temp_path:如果为on,则内容首先被写入临时文件(proxy_temp_path ),然后重命名到proxy_cache_path指定的目录;如果设置为off,则内容直接被写入到proxy_cache_path指定的目录,如果需要cache建议off,该特性是1.7.10提供的。
location配置
location = /cache {
proxy_cache cache;
proxy_cache_key $scheme$proxy_host$request_uri;
proxy_cache_valid 200 5s;
proxy_pass http://backend_tomcat/cache$is_args$args;
add_header cache-status $upstream_cache_status;
}
缓存相关配置:
proxy_cache :指定使用哪个共享内存区域存储缓存键和相关信息;
proxy_cache_key :设置缓存使用的key,默认为访问的完整URL,根据实际情况设置缓存key;
proxy_cache_valid :为不同的响应状态码设置缓存时间;如果是proxy_cache_valid 5s 则200、301、302响应将被缓存;
proxy_cache_valid
proxy_cache_valid不是唯一设置缓存时间的,还可以通过如下方式(优先级从上到下):
1、以秒为单位的“X-Accel-Expires”响应头来设置响应缓存时间;
2、如果没有“X-Accel-Expires”,可以根据“Cache-Control”、“Expires”来设置响应缓存时间;
3、否则使用proxy_cache_valid设置的缓存时间;
如果响应头包含Cache-Control:private/no-cache/no-store、Set-Cookie或者只有一个Vary响应头且其值为*,则响应内容将不会被缓存。可以使用proxy_ignore_headers来忽略这些响应头。
add_headercache-status $upstream_cache_status在响应头中添加缓存命中的状态:
HIT:缓存命中了,直接返回缓存中内容,不回源到后端;
MISS:缓存没有命中,回源到后端获取最新的内容;
EXPIRED:缓存命中但过期了,回源到后端获取最新的内容;
UPDATING:缓存已过期但正在被别的nginx进程更新;配置了proxy_cache_use_staleupdating指令时会存在该状态;
STALE:缓存已过期,但因后端服务出现了问题(比如后端服务挂了)返回过期的响应;配置了如proxy_cache_use_stale error timeout指令后会存在该状态;
REVALIDATED:启用proxy_cache_revalidate指令后,当缓存内容过期时nginx通过一次If-Modified-Since的请求头去验证缓存内容是否过期,此时会返回该状态;
BYPASS:proxy_cache_bypass指令有效时强制回源到后端获取内容,即使已经缓存了;
proxy_cache_min_uses
用于控制请求多少次后响应才被缓存;默认“proxy_cache_min_uses 1;”,如果缓存热点比较集中、存储有限,可以考虑修改该参数以减少缓存数量和写磁盘次数;
proxy_no_cache
用于控制什么情况下响应将不被缓存;比如配置“proxy_no_cache $args_nocache”,如果带的参数值至少有一个不为空或者0,则响应将不被缓存;
proxy_cache_bypass
类似于proxy_no_cache,但是其控制什么情况不从缓存中获取内容,而是直接到后端获取内容;如果命中则$upstream_cache_status为BYPASS;
proxy_cache_use_stale
当对缓存内容的过期时间不敏感,或者后端服务出问题时即使缓存的内容不新鲜也总比返回错误给用户强(类似于托底),此时可以配置该参数,如“proxy_cache_use_stale error timeout http_500 http_502 http_503http_504”:即如果超时、后端连接出错、500、502、503等错误时即使缓存内容已过期也先返回给用户,此时$upstream_cache_status为STALE;还有一个updating表示缓存已过期但正在被别的nginx进程更新将先返回过期的内容,此时 $upstream_cache_status为UPDATING;
proxy_cache_revalidate
当缓存过期后,如果开启了proxy_cache_revalidate,则会发出一次If-Modified-Since和If-None-Match条件请求,如果后端返回304则会得到两个好处:节省带宽和减少写磁盘的次数;此时$upstream_cache_status为REVALIDATED;
proxy_cache_lock
当多个客户端同时请求同一份内容时,如果开启proxy_cache_lock(默认off)则只有一个请求被发送至后端;其他请求将等待该内容返回;当第一个请求返回时,其他请求将从缓存中获取内容返回;当第一个请求超过了proxy_cache_lock_timeout超时时间(默认5s),则其他请求将同时请求到后端来获取响应,且响应不会被缓存(在1.7.8版本之前是被缓存的);启用proxy_cache_lock可以应对Dog-pile effect(当某个缓存失效时,同时又大量相同的请求没命中缓存,而同时请求到后端,从而导致后端压力太大,此时限制一个请求去拿即可)。
proxy_cache_lock_age是1.7.8新添加的,如果在proxy_cache_lock_age指定的时间内(默认5s),最后一个发送到后端进行新缓存构建的请求还没有完成,则下一个请求将被发送到后端来构建缓存(因为1.7.8版本之后,proxy_cache_lock_timeout超时之后返回的内容是不缓存的,需要下一次请求来构建响应缓存)。
清理缓存
有时候缓存的内容是错误的,需要手工清理,nginx plus版本提供了purger的功能,但是对于非plus版本的nginx可以考虑使用ngx_cache_purge(https://github.com/FRiCKLE/ngx_cache_purge)模块进行清理缓存,如:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge cache$1$is_args$args;
}
注意该方法应该只允许内网可以访问,如有必要可以考虑需要密码才能访问。
到此代理层缓存就介绍完了,通过代理层缓存可以解决很多问题,可以参考《京东商品详情页服务闭环实践》和《京东商品详情页服务闭环实践》。
一些经验
1、只缓存200状态码的响应,像302等要根据实际场景决定(比如当系统出错时自动302到错误页面,此时缓存302就不对了);
2、有些页面不需要强一致,可以进行几秒的缓存(比如商品详情页展示的库存,可以缓存几秒钟,短时间的不一致对于用户来说是没有影响的);
3、js/css/image等一些内容缓存时间可以设置的很久(比如1个月甚至1年),通过在页面修改版本来控制过期,不建议随机数方式;
4、假设商品详情页异步加载的一些数据使用的是Last-Modified进行的过期控制,而服务端做了逻辑修改但内容是没有修改的,即内容的最后修改时间没变,如果想过期这些异步加载的数据,可以考虑在商品详情页添加异步加载数据的版本号,通过添加版本号来加载最新的数据,或者将Last-Modified时间加1来解决;而这种情况比较适合使用ETag;
5、商品详情页异步加载的一些数据,可以考虑更长时间的缓存(比如1个月而不是几分钟),可以通过MQ将修改时间推送商品详情页,从而实现按需过期数据;
6、服务端考虑使用内存缓存(tmpfs)、SSD缓存;考虑服务端负载均衡算法,如一致性哈希提升缓存命中率;
7、缓存KEY要合理设计(比如去掉参数/排序参数保证代理层缓存命中),要有清理缓存的工具,出问题时能快速清理掉问题KEY;
8、AB测试/个性化需求时应禁用掉浏览器缓存,但考虑服务端缓存;
9、为了便于查找问题,一般会在响应头中添加源服务器信息,如访问京东商品详情页会看到ser响应头,此头存储了源服务器IP,以便出现问题时知道哪台服务器有问题。
相关阅读
构建亿级前端读服务
应用多级缓存模式支撑海量读服务
构建需求响应式亿级商品详情页
京东商品详情页服务闭环实践
聊聊高并发系统之限流特技-1
聊聊高并发系统之限流特技-2
聊聊高并发系统之降级特技
参考
https://www.w3.org/Protocols/HTTP/1.0/spec.html
https://www.w3.org/Protocols/rfc2616/rfc2616.html
http://nginx.org/en/docs/http/ngx_http_headers_module.html#expires
http://nginx.org/en/docs/http/ngx_http_core_module.html#if_modified_since
http://nginx.org/en/docs/http/ngx_http_core_module.html#etag
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
简介
最近遇到很多人来咨询我关于浏览器缓存的一些问题,而这些问题都是类似的,因此总结本文来解答以后遇到类似问题的朋友。
因本文主要以浏览器缓存场景介绍,所以非浏览器场景下的一些用法本文不会介绍,而且本文以chrome为测试浏览器。
浏览器缓存是指当我们使用浏览器访问一些网站页面或者http服务时,根据服务端返回的缓存设置响应头将响应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务端验证内容是否过期即可。这样的好处可以减少浏览器和服务端之间来回传输的数据量,节省带宽提升性能。
首先看个例子;当我们第一次访问http://item.jd.com/1856588.html时将得到如下响应头:
然后接着按F5刷新页面,将得到如下响应头
第二次返回的相应状态码为304,表示服务端文档没有修过过,浏览器缓存的内容还是最新的。
接下来我们看下如何在Java应用层控制浏览器缓存。
示例
Last-Modified
如下是我们的spring mvc缓存测试代码:
@RequestMapping("/cache")
public ResponseEntity<String> cache(
HttpServletRequest request,
//为了方便测试,此处传入文档最后修改时间
@RequestParam("millis") long lastModifiedMillis,
//浏览器验证文档内容是否修改时传入的Last-Modified
@RequestHeader (value = "If-Modified-Since", required = false) Date ifModifiedSince) {
//当前系统时间
long now = System.currentTimeMillis();
//文档可以在浏览器端/proxy上缓存多久
long maxAge = 20;
//判断内容是否修改了,此处使用等值判断
if(ifModifiedSince != null && ifModifiedSince.getTime() == lastModifiedMillis) {
return new ResponseEntity<String>(HttpStatus.NOT_MODIFIED);
}
DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
String body = "<a href=''>点击访问当前链接</a>";
MultiValueMap<String, String> headers = new HttpHeaders();
//文档修改时间
headers.add("Last-Modified", gmtDateFormat.format(new Date(lastModifiedMillis)));
//当前系统时间
headers.add("Date", gmtDateFormat.format(new Date(now)));
//过期时间 http 1.0支持
headers.add("Expires", gmtDateFormat.format(new Date(now + maxAge)));
//文档生存时间 http 1.1支持
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<String>(body, headers, HttpStatus.OK);
}
为了方便测试,测试时将文档的修改时间通过millis参数传入,实际应用时可以使用如商品的最后修改时间等替代。
首次访问
首次访问http://localhost:9080/cache?millis=1471349916709,将得到如下响应头:
响应状态码200表示请求内容成功,另外有如下几个缓存控制参数:
Last-Modified:表示文档的最后修改时间,当去服务器验证时会拿这个时间去;
Expires:http/1.0规范定义,表示文档在浏览器中的过期时间,当缓存的内容超过这个时间则需要重新去服务器获取最新的内容;
Cache-Control:http/1.1规范定义,表示浏览器缓存控制,max-age=20表示文档可以在浏览器中缓存20秒。
根据规范定义Cache-Control优先级高于Expires;实际使用时可以两个都用,或仅使用Cache-Control就可以了(比如京东的活动页sale.jd.com)。一般情况下Expires=当前系统时间(Date) + 缓存时间(Cache-Control: max-age)。大家可以在如上测试代码进行两者单独测试,缓存都是可行的。
F5刷新
接着按F5刷新当前页面,将看到浏览器发送如下请求头:
此处发送时有一个If-Modified-Since请求头,其值是上次请求响应中的Last-Modified,即浏览器会拿这个时间去服务端验证内容是否发生了变更。接着收到如下响应信息:
响应状态码为304,表示服务端告诉浏览器说“浏览器你缓存的内容没有变化,直接使用缓存内容展示吧”。
注:在测试时要过一段时间更改下参数millis来表示内容修改了,要不然会一直看到304响应。
Ctrl+F5强制刷新
如果你想强制从服务端获取最新的内容,可以按Ctrl+F5:
浏览器在请求时不会带上If-Modified-Since,并带上Cache-Control:no-cache和Pragma:no-cache,这是为了告诉服务端说我请给我一份最新的内容。
from cache
当我们按F5刷新、Ctrl+F5强制刷新、地址栏输入地址刷新时都会去服务端验证内容是否发生了变更。那什么情况才不去服务端验证呢?即有些朋友还会发现有一些“from cache”的情况,这是什么情况下发生的呢?
从A页面跳转到A页面或者从A页面跳转到B页面时:
大家可以通过如上方式模拟,即从A页面跳转到A页面也是情况1。此时如果内容还在缓存时间之内,直接从浏览器获取的内容,而不去服务端验证。
访问页面http://item.jd.com/11056556.html,然后点击面包屑中的HTTP权威指南时会跳转到当前页面,此时看到如下结果,页面及页面异步加载的一些js、css、图片都from cache了。
还有如通过浏览器历史记录进行前进后退时也会走from cache。本文是基于chrome 52.0.2743.116 m版本测试,不同浏览器行为可能存在差异。
Age
一般用于代理层(如CDN),大家在访问京东一些页面时会发现有一个Age响应头,然后强制刷新(Ctrl+F5)后会发现其不断的变化;其表示此内容在代理层从缓存到现在经过了多长时间了,即在代理层缓存了多长时间了。
Vary
一般用于代理层(如CDN),用于代理层和浏览器协商什么情况下使用哪个版本的缓存内容(比如压缩版和非压缩版),即什么情况下后续请求才能使用代理层缓存的该版本内容,比如如下响应是告知浏览器Content-Encoding:gzip,即缓存代理层缓存了gzip版本的内容;那么后续的请求在请求时Accept-Encoding头部中包含gzip时才能使用改代理层缓存。
Via
一般用于代理层(如CDN),表示访问到最终内容经过了哪些代理层,用的什么协议,代理层是否缓存命中等等;通过它可以进行一些故障诊断。
ETag
@RequestMapping("/cache/etag")
public ResponseEntity<String> cache(
HttpServletRequest request,
HttpServletResponse response,
//浏览器验证文档内容的实体 If-None-Match
@RequestHeader (value = "If-None-Match", required = false) String ifNoneMatch) {
//当前系统时间
long now = System.currentTimeMillis();
//文档可以在浏览器端/proxy上缓存多久
long maxAge = 10;
String body = "<a href=''>点击访问当前链接</a>";
//弱实体
String etag = "W/\"" + md5(body) + "\"";
if(StringUtils.equals(ifNoneMatch, etag)) {
return new ResponseEntity<String>(HttpStatus.NOT_MODIFIED);
}
DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
MultiValueMap<String, String> headers = new HttpHeaders();
//ETag http 1.1支持
headers.add("ETag", etag);
//当前系统时间
headers.add("Date", gmtDateFormat.format(new Date(now)));
//文档生存时间 http 1.1支持
headers.add("Cache-Control", "max-age=" + maxAge);
return new ResponseEntity<String>(body, headers, HttpStatus.OK);
}
其中ETag用于发送到服务端进行内容变更验证的,而Catch-Control是用于控制缓存时间的(浏览器、代理层等)。此处我们使用了弱实体W\”343sda”,弱实体(”343sda”)只要内容语义没变即可,比如内容的gzip版和非gzip版可以使用弱实体验证;而强实体指字节必须完全一致(gzip和非gzip情况是不一样的),因此建议首先选择使用弱实体。nginx在生成etag时使用的算法是Last-Modified + Content-Length计算的:
ngx_sprintf(etag->value.data,"\"%xT-%xO\"",
r->headers_out.last_modified_time,
r->headers_out.content_length_n)
到此简单的基于文档修改时间和过期时间的缓存控制就介绍完了,在内容型响应我们大多数根据内容的修改时间来进行缓存控制,ETag根据实际需求而定(比如)。另外还可以使用html Meta标签控制浏览器缓存,但是对代理层缓存无效,因此不建议使用。
总结
1、服务端响应的Last-Modified会在下次请求时以If-Modified-Since请求头带到服务端进行文档是否修改的验证,如果没有修改则返回304,浏览器可以直接使用缓存内容;
2、Cache-Control:max-age和Expires用于决定浏览器端内容缓存多久,即多久过期,过期后则删除缓存重新从服务端获取最新的;另外可以用于from cache场景;
3、http/1.1规范定义的Cache-Control优先级高于http/1.0规范定义的Expires;
4、一般情况下Expires=当前系统时间 + 缓存时间(Cache-Control:max-age);
5、http/1.1规范定义了ETag来通过文档摘要的方式控制。
Last-Modified与ETag同时使用时,浏览器在验证时会同时发送If-Modified-Since和If-None-Match,按照http/1.1规范,如果同时使用If-Modified-Since和If-None-Match则服务端必须两个都验证通过后才能返回304;且nginx就是这样做的。因此实际使用时应该根据实际情况选择。还有If-Match和If-Unmodified-Since本文就不介绍了。
接下来我们看下如何使用nginx进行缓存控制。
nginx缓存设置
nginx提供了expires、etag、if-modified-since指令来进行浏览器缓存控制。
expires
假设我们使用nginx作为静态资源服务器,此时可以使用expires进行缓存控制。
location /img {
alias /export/img/;
expires 1d;
}
当我们访问静态资源时,如http://192.168.61.129/img/1.jpg,将得到类似如下的响应头:
对于静态资源会自动添加ETag,可以通过添加“etag off”指令禁止生成ETag。如果是静态文件Last-Modified是文件的最后修改时间;Expires是根据当前服务端系统时间算出来的。如上nginx配置的计算逻辑(实际计算逻辑比这个多,具体参考官方文档):
if (expires == NGX_HTTP_EXPIRES_ACCESS ||r->headers_out.last_modified_time == -1) {
max_age = expires_time;
expires_time += now;
}
if-modified-since
此指令用于表示nginx如何拿服务端的Last-Modified和浏览器端的If-Modified-Since时间进行比较,默认“if_modified_since exact”表示精确匹配,也可以使用“if_modified_sincebefore”表示只要文件的上次修改时间早于或等于浏览器短的If-Modified-Since时间,就返回304。
nginx proxy expires
使用nginx作为反向代理时,请求会先进入nginx,然后nginx将请求转发给后端应用。如下图所示:
首先配置upstream:
upstream backend_tomcat {
server 192.168.61.1:9080 max_fails=10 fail_timeout=10s weight=5;
}
接着配置location:
location = /cache {
proxy_pass http://backend_tomcat/cache$is_args$args;
}
接下来我们可以通过如http://192.168.61.129/cache?millis=1471349916709访问nginx,nginx会将请求转发给后端java应用。也就是说nginx只是做了相关的转发(负载均衡),并没有对请求和响应做什么处理。
假设对后端返回的过期时间需要调整,可以添加expires指令到location:
location = /cache {
proxy_pass http://backend_tomcat/cache$is_args$args;
expires 5s;
}
然后再请求相关的URL,将得到如下响应:
过期时间相关的响应头被expires指令更改了,但是Last-Modified是没有变的。
即使我们更改了缓存过期头,但nginx本身没有对这些内容做缓存,每次请求还是要到后端验证的,假设在过期时间内,这些验证在nginx这一层验证就可以了,不需要到后端验证,这样可以减少后端的很大压力。即整体流程是:
1、浏览器发起请求,首先到nginx,nginx根据url在nginx本地查找是否有文档缓存;
2、nginx没有找到本地缓存,则去后端获取最新的文档,并放入到nginx本地缓存中;返回200状态码和最新的文档给浏览器;
3、nginx找到本地缓存了,首先验证文档是否过期(Cache-Control:max-age=5),如果过期则去后端获取最新的文档,并放入nginx本地缓存中,返回200状态码和最新的文档给浏览器;如果文档没有过期,如果If-Modified-Since与缓存文档的Last-Modified匹配,则返回300状态码给浏览器,否则返回200状态码和最新的文档给浏览器。
即内容不需要动态(计算、渲染等)速度更快,内容越接近于用户速度越快。像apache traffic server、squid、varnish、nginx等技术都可以来进行内容缓存。还有CDN就是用来加速用户访问的:
即用户首先访问到全国各地的CDN节点(使用如ATS、Squid实现),如果CDN没命中,会回源到中央nginx集群,该集群如果没有命中缓存(该集群的缓存不是必须的,要根据实际命中情况等决定),最后回源到后端应用集群。
像我们商品详情页的一些服务就大量使用了nginx缓存减少回源到后端的请求量,从而提升访问速度。可以参考《构建需求响应式亿级商品详情页》、《京东商品详情页服务闭环实践》和《应用多级缓存模式支撑海量读服务》。
nginx代理层缓存
http模块配置:
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 512 4k;
proxy_busy_buffers_size 64k;
proxy_cache_path /export/cache/proxy_cachelevels=1:2 keys_zone=cache:512m inactive=5m max_size=8g use_temp_path=off;
#proxy timeout
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
其中红色部分是proxy_cache_path指令相关配置:
levels=1:2 :表示创建两级目录结构,比如/export/cache/proxy_cache/7/3c/,将所有文件放在一级目录结构中如果文件量很大会导致访问文件慢;
keys_zone=cache:512m :设置存储所有缓存key和相关信息的共享内存区,1M大约能存储8000个key;
inactive=5m :inactive指定被缓存的内容多久不被访问将从缓存中移除,以保证内容的新鲜;默认10分钟;
max_size=8g :最大缓存阀值,“cachemanager”进程会监控最大缓存大小,当缓存达到该阀值,该进程将从缓存中移除最近最少使用的内容;
use_temp_path:如果为on,则内容首先被写入临时文件(proxy_temp_path ),然后重命名到proxy_cache_path指定的目录;如果设置为off,则内容直接被写入到proxy_cache_path指定的目录,如果需要cache建议off,该特性是1.7.10提供的。
location配置
location = /cache {
proxy_cache cache;
proxy_cache_key $scheme$proxy_host$request_uri;
proxy_cache_valid 200 5s;
proxy_pass http://backend_tomcat/cache$is_args$args;
add_header cache-status $upstream_cache_status;
}
缓存相关配置:
proxy_cache :指定使用哪个共享内存区域存储缓存键和相关信息;
proxy_cache_key :设置缓存使用的key,默认为访问的完整URL,根据实际情况设置缓存key;
proxy_cache_valid :为不同的响应状态码设置缓存时间;如果是proxy_cache_valid 5s 则200、301、302响应将被缓存;
proxy_cache_valid
proxy_cache_valid不是唯一设置缓存时间的,还可以通过如下方式(优先级从上到下):
1、以秒为单位的“X-Accel-Expires”响应头来设置响应缓存时间;
2、如果没有“X-Accel-Expires”,可以根据“Cache-Control”、“Expires”来设置响应缓存时间;
3、否则使用proxy_cache_valid设置的缓存时间;
如果响应头包含Cache-Control:private/no-cache/no-store、Set-Cookie或者只有一个Vary响应头且其值为*,则响应内容将不会被缓存。可以使用proxy_ignore_headers来忽略这些响应头。
add_headercache-status $upstream_cache_status在响应头中添加缓存命中的状态:
HIT:缓存命中了,直接返回缓存中内容,不回源到后端;
MISS:缓存没有命中,回源到后端获取最新的内容;
EXPIRED:缓存命中但过期了,回源到后端获取最新的内容;
UPDATING:缓存已过期但正在被别的nginx进程更新;配置了proxy_cache_use_staleupdating指令时会存在该状态;
STALE:缓存已过期,但因后端服务出现了问题(比如后端服务挂了)返回过期的响应;配置了如proxy_cache_use_stale error timeout指令后会存在该状态;
REVALIDATED:启用proxy_cache_revalidate指令后,当缓存内容过期时nginx通过一次If-Modified-Since的请求头去验证缓存内容是否过期,此时会返回该状态;
BYPASS:proxy_cache_bypass指令有效时强制回源到后端获取内容,即使已经缓存了;
proxy_cache_min_uses
用于控制请求多少次后响应才被缓存;默认“proxy_cache_min_uses 1;”,如果缓存热点比较集中、存储有限,可以考虑修改该参数以减少缓存数量和写磁盘次数;
proxy_no_cache
用于控制什么情况下响应将不被缓存;比如配置“proxy_no_cache $args_nocache”,如果带的参数值至少有一个不为空或者0,则响应将不被缓存;
proxy_cache_bypass
类似于proxy_no_cache,但是其控制什么情况不从缓存中获取内容,而是直接到后端获取内容;如果命中则$upstream_cache_status为BYPASS;
proxy_cache_use_stale
当对缓存内容的过期时间不敏感,或者后端服务出问题时即使缓存的内容不新鲜也总比返回错误给用户强(类似于托底),此时可以配置该参数,如“proxy_cache_use_stale error timeout http_500 http_502 http_503http_504”:即如果超时、后端连接出错、500、502、503等错误时即使缓存内容已过期也先返回给用户,此时$upstream_cache_status为STALE;还有一个updating表示缓存已过期但正在被别的nginx进程更新将先返回过期的内容,此时 $upstream_cache_status为UPDATING;
proxy_cache_revalidate
当缓存过期后,如果开启了proxy_cache_revalidate,则会发出一次If-Modified-Since和If-None-Match条件请求,如果后端返回304则会得到两个好处:节省带宽和减少写磁盘的次数;此时$upstream_cache_status为REVALIDATED;
proxy_cache_lock
当多个客户端同时请求同一份内容时,如果开启proxy_cache_lock(默认off)则只有一个请求被发送至后端;其他请求将等待该内容返回;当第一个请求返回时,其他请求将从缓存中获取内容返回;当第一个请求超过了proxy_cache_lock_timeout超时时间(默认5s),则其他请求将同时请求到后端来获取响应,且响应不会被缓存(在1.7.8版本之前是被缓存的);启用proxy_cache_lock可以应对Dog-pile effect(当某个缓存失效时,同时又大量相同的请求没命中缓存,而同时请求到后端,从而导致后端压力太大,此时限制一个请求去拿即可)。
proxy_cache_lock_age是1.7.8新添加的,如果在proxy_cache_lock_age指定的时间内(默认5s),最后一个发送到后端进行新缓存构建的请求还没有完成,则下一个请求将被发送到后端来构建缓存(因为1.7.8版本之后,proxy_cache_lock_timeout超时之后返回的内容是不缓存的,需要下一次请求来构建响应缓存)。
清理缓存
有时候缓存的内容是错误的,需要手工清理,nginx plus版本提供了purger的功能,但是对于非plus版本的nginx可以考虑使用ngx_cache_purge(https://github.com/FRiCKLE/ngx_cache_purge)模块进行清理缓存,如:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge cache$1$is_args$args;
}
注意该方法应该只允许内网可以访问,如有必要可以考虑需要密码才能访问。
到此代理层缓存就介绍完了,通过代理层缓存可以解决很多问题,可以参考《京东商品详情页服务闭环实践》和《京东商品详情页服务闭环实践》。
一些经验
1、只缓存200状态码的响应,像302等要根据实际场景决定(比如当系统出错时自动302到错误页面,此时缓存302就不对了);
2、有些页面不需要强一致,可以进行几秒的缓存(比如商品详情页展示的库存,可以缓存几秒钟,短时间的不一致对于用户来说是没有影响的);
3、js/css/image等一些内容缓存时间可以设置的很久(比如1个月甚至1年),通过在页面修改版本来控制过期,不建议随机数方式;
4、假设商品详情页异步加载的一些数据使用的是Last-Modified进行的过期控制,而服务端做了逻辑修改但内容是没有修改的,即内容的最后修改时间没变,如果想过期这些异步加载的数据,可以考虑在商品详情页添加异步加载数据的版本号,通过添加版本号来加载最新的数据,或者将Last-Modified时间加1来解决;而这种情况比较适合使用ETag;
5、商品详情页异步加载的一些数据,可以考虑更长时间的缓存(比如1个月而不是几分钟),可以通过MQ将修改时间推送商品详情页,从而实现按需过期数据;
6、服务端考虑使用内存缓存(tmpfs)、SSD缓存;考虑服务端负载均衡算法,如一致性哈希提升缓存命中率;
7、缓存KEY要合理设计(比如去掉参数/排序参数保证代理层缓存命中),要有清理缓存的工具,出问题时能快速清理掉问题KEY;
8、AB测试/个性化需求时应禁用掉浏览器缓存,但考虑服务端缓存;
9、为了便于查找问题,一般会在响应头中添加源服务器信息,如访问京东商品详情页会看到ser响应头,此头存储了源服务器IP,以便出现问题时知道哪台服务器有问题。
相关阅读
构建亿级前端读服务
应用多级缓存模式支撑海量读服务
构建需求响应式亿级商品详情页
京东商品详情页服务闭环实践
聊聊高并发系统之限流特技-1
聊聊高并发系统之限流特技-2
聊聊高并发系统之降级特技
参考
https://www.w3.org/Protocols/HTTP/1.0/spec.html
https://www.w3.org/Protocols/rfc2616/rfc2616.html
http://nginx.org/en/docs/http/ngx_http_headers_module.html#expires
http://nginx.org/en/docs/http/ngx_http_core_module.html#if_modified_since
http://nginx.org/en/docs/http/ngx_http_core_module.html#etag
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
相关推荐
MySQL是世界上最流行的开源关系型数据库管理系统之一,其设计目标是处理大量数据,同时提供高并发和高可用性。以下是一些关键知识点: 1. **SQL语句执行流程**:当提交一个SQL语句时,MySQL会经历解析、预处理、...
7. **性能优化**:通过缓存策略、数据库索引等手段,提升系统的整体性能,尤其是高并发情况下的表现。 8. **扩展性**:考虑到未来功能的拓展和用户数量的增长,系统需要具备一定的扩展性,能够方便地添加新的功能...
4、多机房部署架构下的4级缓存架构:大公司里真实的亿级流量高并发系统,都是采取了多个机房的部署架构,以实现高可用以及异地灾备。课程会重点讲解,在多机房部署架构下,如何设计和实现高并发系统的4级缓存架构。 ...
5. **分布式架构**:在处理高并发和大数据量时,分布式架构成为必然选择。它通过负载均衡、缓存策略、分布式数据库等手段,提升系统的处理能力和容错能力。 6. **容器化与Docker**:Docker提供了一种标准化的打包...
7. **使用场景**:`ConcurrentHashMap`适用于多线程环境下需要高并发读写操作的情况,如缓存系统、分布式系统中的数据存储等。如果你需要在并发环境中保证数据一致性,同时追求高性能,`ConcurrentHashMap`是一个...
PPT.pptx可能包含更深入的Redis线程模型讲解,例如如何利用事件循环处理客户端请求,以及在高并发场景下如何保持性能。笔记.docx可能详细记录了学习过程中的重点和理解,包括Redis的数据类型、命令、持久化方式等。 ...
但是今天我们不聊它们,我们来聊一聊如何实现本地缓存。 要实现一个较好的本地缓存,需要从三个方面开始: 1、存储集合的选择 存储集合的选择是实现本地缓存的第一步。我们需要选择一个合适的存储集合来存储我们...
100,000〜4,000,000 QPS,使用二进制搜索技术/《 VPCS架构》德塔公司开源缓存服务器存储上亿级量子碎片,带有并发映射表的微型Java线程小型安全HTTP json高速缓存服务器,并发映射表,2分法字符串索引,高并发安全...
【性能优化】由于实时通信对性能要求较高,系统可能采用了缓存技术减少数据库访问,优化Ajax请求,以及使用WebSocket等技术实现双向实时通信,提高用户体验。 总的来说,“JSP+EXT超强仿QQ聊天系统 WEBQQ”是一个...
- **高并发处理**:社交应用往往面临高并发访问的问题,因此需要利用PHP结合缓存机制(如Redis)来提高系统处理能力。 - **数据处理与存储**:考虑到数据量庞大,后端需要对数据进行高效的处理和存储。可以采用消息...
- 针对高并发场景,设计可水平扩展的架构,如负载均衡器分发请求到多个服务器实例。 - 使用消息队列如RabbitMQ或Azure Service Bus处理异步任务,避免阻塞主线程。 7. **性能优化**: - 数据库查询优化,减少...
其核心设计思想是将业务逻辑与网络IO分离,通过worker进程处理业务,而gateway进程负责网络通信,这样可以实现高并发和低延迟。在聊天系统中,GatewayWorker用于接收客户端的连接,处理用户的登录、发送消息、接收...
- 应用场景分析:针对不同的业务需求,如高并发、大数据量、实时查询等,如何选择合适的MySQL架构。 3. **老大让你设计系统缓存架构,你能Hold住吗?.pptx** - 缓存架构基础:讲解缓存的基本原理和类型,如内存...
考虑到社交类APP用户在线率、高并发、数据量庞大。 数据库方面: 要用到多个数据库联合,mysql 用作写,mongodb 用作读,实现读写分离,数据库合理设计,数据库水平切分。 缓存、集群、分布式、CDN基于情况上。 ...
Node.js的非阻塞I/O模型和事件驱动机制使其在处理高并发请求时表现出色,适用于构建实时聊天功能。 2. Express:Express是Node.js的一个轻量级框架,用于构建Web应用程序。它简化了HTTP服务器的创建和路由管理,...
高性能的缓存机制,确保网站访问速度; 网站支持全站页面生成静态化功能; 强大的安全机制,防止各种注入式攻击等漏洞; 充分考虑了底层大数据量并发的性能问题; 图片上传支持云存储解决方案,不占带宽,无并发...
在设计一个海量在线用户的即时通讯系统时,首要目标是确保高效、稳定且具有高可用性的通信体验。以下是对文档中提到的关键知识点的详细说明: 1. **服务端设计** - **总体架构**:该系统分为五个层级。第一层是...
E聊可能采用了关系型或非关系型数据库(如MySQL、MongoDB等),以支持高并发、高可用的数据存储需求。 7. **安全与性能优化**:E聊SDK应考虑了安全问题,比如使用HTTPS进行数据传输,防止中间人攻击;同时,为了...