bboyjing's blog

跟开涛学架构七【HTTP缓存】

本章节来学习下Http缓存相关内容,这也是容易被忽略的一部分。我们利用Chrome和real_server_1项目进行测试。我们下面就来看下如何在Java应用层控制浏览器缓存。

HTTP缓存

Last-Modified

直接看real_server_1上实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@RestController
@RequestMapping
public class HttpCacheController {
Cache<String, Long> lastModifiedCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS).build();
@RequestMapping(path = "/last-modified", method = RequestMethod.GET)
public ResponseEntity<String> lastModified(@RequestHeader(value = "If-Modified-Since", required = false) Date ifModifiedSince) throws ExecutionException {
DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
// 文档最后修改时间,去掉毫秒值
long lastModifiedMillis = getLastModified() / 1000 * 1000;
// 当前系统时间,去掉毫秒值
long now = System.currentTimeMillis() / 1000 * 1000;
// 文档可以被缓存多久,单位秒
long maxAge = 20;
MultiValueMap<String, String> headers = new HttpHeaders();
// 当前时间
headers.add("Date", gmtDateFormat.format(new Date(now)));
// 过期时间 http1.0支持
headers.add("Expires", gmtDateFormat.format(new Date(now + maxAge * 1000)));
// 文档生存时间 http1.1支持
headers.add("Cache-Control", "max-age=" + maxAge);
// 判断内容是否修改了
if (ifModifiedSince != null && ifModifiedSince.getTime() == lastModifiedMillis) {
return new ResponseEntity(headers, HttpStatus.NOT_MODIFIED);
} else {
// 文档修改时间
headers.add("Last-Modified", gmtDateFormat.format(lastModifiedMillis));
String body = "<a href = ''>点击跳转当前链接</a>";
return new ResponseEntity(body, headers, HttpStatus.OK);
}
}
public long getLastModified() throws ExecutionException {
return lastModifiedCache.get("lastModified", () -> System.currentTimeMillis());
}
}

为了方便测试,文档的修改时间为每十秒钟更新一次。下面就来测试下:

首次访问

首次访问http://localhost:8081/last-modified,将得到如下响应头:
hunger_6
响应状态吗200表示请求成功,另外,有如下几个缓存控制参数。

  • Last-Modified:表示文档的最后修改时间,当去服务器验证时会用到这个时间
  • Expires:http/1.0规范定义,表示文档在浏览器中的过期时间,当缓存内容时间超过这个时间,则需要重新去服务器获取最新的内容。
  • Cache-Control:http/1.1规范定义,表示浏览器缓存控制,max-age=20表示文档可以在浏览器中缓存20秒

根据规范定义,Cache-Control优先级高于Expires。实际使用时可以两个都用,或仅使用Cache-Control就可以了。

F5刷新

接着按F5刷新当前页面(拒首次访问10秒内),将看到浏览器发送如下请求头:
hunger_7
此处发送时有一个If-Modified-Since请求头,其值是上次请求响应中的Last-Modified,即浏览器会用这个时间去服务端验证内容是否发生了变更,接着收到如下响应信息:
hunger_8
状态码为304,表示服务端通知浏览器”你缓存的内容没有变化,直接使用缓存内容展示吧”。

Ctrl+F5强制刷新

如果想强制从服务端获取最新的内容,可以按”Ctrl + F5”(“command + shift + R”)组合键。浏览器在请求时不会带上If-Modified-Since,但是会带上Cache-Control:no-cache和Pragma:no-cache,这是为了通知服务器提供一份最新的内容。

ETag

先直接看下etag实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping(path = "/etag", method = RequestMethod.GET)
public ResponseEntity<String> etag(@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) throws ExecutionException {
// 当前系统时间
long now = System.currentTimeMillis();
// 文档可以在浏览器上缓存多久
long maxAge = 10;
String body = "<a href = ''>点击跳转当前链接</a>";
String etag = "W/\"" + MD5Util.MD5(body) + "\"";
if (StringUtils.equals(ifNoneMatch, etag)) {
return new ResponseEntity(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(body, headers, HttpStatus.OK);
}

其中Etag用于发送到服务器进行内容变更验证的,而Cache-Control用于控制缓存时间。此处使用了W\”343sda”,弱实体只要内容语义没变即可。Nginx在生成Etag时使用的算法是Last-Modified + Content-Length。经过测试,可以看出请求头带上了If-None-Match,其值为第一次访问http://localhost:8081/etag时返回的Response Headers的Etag的值。

到目前为止我们一直没有看出Cache-Control的max-age或者Expires是如何起作用的。之前测试都是在浏览器地址栏刷新或者强制刷新当前请求,都会去服务端验证内容是否发生变更,跟max-age完全没关系。也就是说max-age起作用的点不在于当前请求,而是可以被别人缓存多久。典型的场景比如js、css、图片等外部资源的引用,或者页面跳转等。我们拿页面跳转来做个测试,修改下/etag请求的body部分:String body = "<a href = 'http://localhost:8081/last-modified'>点击跳转到/last-modified</a>";,目的是为了看出max-age到底是对谁生效。理一下测试的场景,我们当前主页面为http://localhost:8081/etag,其内容就是跳转到http://localhost:8081/last-modified,当前页面的max-age为10秒,/last-modified页面的max-age为20秒。下面列出测试步骤

  1. 强制刷新http://localhost:8081/etag,然后立刻点击超链接,此时会跳转到http://localhost:8081/last-modified,看下请求头:
    hunger_9
  2. 点击浏览器后退按钮回到http://localhost:8081/etag,然后再点击超链接,请求到/last-modified的请求头:
    hunger_10
    可以看出Request Headers和之前大不相同,再结合服务端的debug来看,浏览器其实走的是本地cache,没有向服务端发出请求。当距离第一步20秒左右之后,再次执行本步奏,会发现点击超链接时,会发送/last-modified请求到服务端。综上看来这是max-age起的作用,其意思是/last-modified链接的内容可以被/etag缓存20秒。

Nginx HTTP缓存设置

Nginx托管的静态资源

Nginx 提供了expires、etag、if-modified-since指令来实现浏览器缓存控制。
我们下面来测试下:

1
2
3
4
5
6
7
# 1、修改conf/nginx.conf
location /img/ {
alias /usr/local/Cellar/openresty/1.11.2.5/nginx/html/;
expires 1d;
}
# 2、在/usr/local/Cellar/openresty/1.11.2.5/nginx/html/目录下新增一张图片1.jpg

访问http://localhost/img/1.jpg,返回的Response Headers如下:
hunger_11
可见缓存所需要的请求都基本都有了,其中对于静态资源Nginx会自动添加Etag,可以通过etag off指令禁止生成Etag。

proxy_pass

在real_server_1项目中写一个新的请求,来看看nginx对proxy_pass的默认行为:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(path = "/cache_nginx", method = RequestMethod.GET)
public String cache_nginx() {
return "from real server 1.";
}
# Nginx location配置,和上面一样,加了expires
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://real_server;
expires 1d;
}

来看下访问http://localhost/cache_nginx,返回的Response Headers如下:
hunger_12
可以看出expires 1d的设置是成功的,但是和Nginx对静态资源的处理有所不同。proxy_pass的Response Headers没有了ETag和Last-Modified,交由真正业务服务器自行处理。

Http缓存就学到这里,在实际使用中,index页面和其加载的外部资源该如何缓存,需要自行斟酌。同时别忘了还要结合是通过浏览器直接访问还是APP的webview访问,这两种方式对于index页面的访问行为有时候是不一样的,区别就在于max-age是否介入。