Http缓存总结
强制缓存与协商缓存
强制缓存:本地缓存
当浏览器二次请求服务器资源时,浏览器直接从本地缓存中读取并返回200,
不会与服务器进行交互
协商缓存:浏览器根据HTTP响应头里面的内容,来采取对应的缓存方式
如果用户访问的资源在上次响应中携带了协商缓存相关的头,
浏览器会携带相关字段并向服务端确认缓存的资源是否有效。
如果服务端判断该资源有效,则返回304,携带相关缓存头但不返回资源内容,
浏览器直接使用本地缓存。
否则返回200,服务器返回新的资源内容。
强制缓存
Expires
Expires是HTTP 1.0出现的响应头,它的值是GMT格式的时间字符串,
如Expires: Sun Dec 24 2022 18:01:30 GMT。这个时间代表资源的过期失效时间,
在这个时间之前,浏览器访问该资源时始终使用强缓存。
缺点:Expires是个相对时间(客户端自己修改了本地时间)缓存失效。
代码实现
@RequestMapping("/v3/{id}.json")
public Map<String,String> show3(@PathVariable("id") String id,HttpServletResponse response) {
Map<String,String> map = new HashMap<>();
map.put("id", id);
map.put("show3 name", "user:"+id);
/**
* 格式化一个GMT(格林尼治时间) 时间
*
* 转化后的GMT 有时差问题和本地不一致 但不影响缓存
*/
Date date = new Date(System.currentTimeMillis() + 10000); //10秒后过期
DateFormat format = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss z", Locale.ENGLISH);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String str = format.format(date);
response.addHeader("Expires", str);
System.out.println("tach server v3@@@@@@@@@@@@@@@@@");
System.out.println("tach server v3@@@@@@@@@@@@@@@@@");
return map;
}
Cache-Control
Cache-Control是HTTP 1.1出现的响应头,它可以用在请求头和响应头中,该指令是单向的,意味着在请求中设置的指令,在响应中可能没有。
常用的值有max-age、no-cache和no-store
代码实现
@RequestMapping("/v2/{id}.json")
public Map<String,String> show2(@PathVariable("id") String id,HttpServletResponse response) {
Map<String,String> map = new HashMap<>();
map.put("id", id);
map.put("show2 name", "user:"+id);
/**
* 设置过期时间为10秒
* 10秒内多次请求 /v2/1.json 不会触发打印tach server v2@@@@@@@@@@@@@@@@@
* 因为浏览器没发送请求
*
* 注意:
* 1 浏览器使用firefox,用chrome 没有效果
* 2 浏览器不能打开调试模式 F12 ,调试模式下max-age无效
*/
response.addHeader("Cache-Control", "max-age=10");
System.out.println("tach server v2@@@@@@@@@@@@@@@@@");
System.out.println("tach server v2@@@@@@@@@@@@@@@@@");
return map;
}
协商缓存
Last-Modified/If-Modified-Since
Last-Modified代表的是资源的最后修改时间,是一个GMT格式的时间。服务器在返回资源时,如果写到Last-Modified,则浏览器在后续对该资源的访问的请求头里都会携带If-Modified-Since,值与服务器返回的Last-Modified值相同。服务端通过该值来判断浏览器请求的该资源是否有修改,如果有,则返回修改后的内容,且返回200,否则返回304,body中没有内容。
代码实现
@RequestMapping("/v4/{id}.json")
public Map<String,String> show4(@PathVariable("id") String id,
HttpServletRequest request,
HttpServletResponse response) {
/**
* 如果第一次请求中有Last-Modified
* 则浏览器在后续对该资源的访问的请求头里都会携带If-Modified-Since
* 值与服务器返回的Last-Modified值相同
*/
String ifModifiedSince = request.getHeader("If-Modified-Since");
/**
* 第一次请求没有If-Modified-Since
*/
boolean doCache = true;
if(ifModifiedSince == null || ifModifiedSince.trim().equals("") || ifModifiedSince.equals("null")) {
doCache = false;
} else {
/**
* 转化下浏览器发送的时间
*/
DateFormat format = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss z", Locale.ENGLISH);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
Date d = null;
try {
d = format.parse(ifModifiedSince);
} catch (ParseException e) {
}
/**
* 当前时间大于缓存过期时间
*/
doCache = System.currentTimeMillis() > d.getTime();
}
if(doCache) {
/**
* 返回304并且 body没有内容,减少数据测传输
*/
response.setStatus(304);
return null;
}else {
Map<String,String> map = new HashMap<>();
map.put("id", id);
map.put("show4 name", "user:"+id + " sys: " + System.currentTimeMillis());
/**
* 格式化一个GMT(格林尼治时间) 时间
*/
Date date = new Date(System.currentTimeMillis() + 20000); //20秒后过期
DateFormat format = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss z", Locale.ENGLISH);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String str = format.format(date);
response.addHeader("Last-Modified", str);
try {
/**
* 模拟http请求很耗时
* Last-Modified/If-Modified-Since 可以节约这部分时间 和 减少响应数据包的大小
*/
Thread.sleep(3000L);
} catch (InterruptedException e) {
}
return map;
}
}
ETag/If-None-Match
服务端需要根据资源做一个hash计算,把hash值作为ETag返回给客户端
后续客户端对该资源的访问的请求头里都会携带If-None-Match,值与服务器返回的ETag值相同。
服务端需要比较值来判断是否缓存有效
代码实现
@RequestMapping("/v5/{id}.json")
public String show5(@PathVariable("id") String id,
HttpServletRequest request,
HttpServletResponse response) {
/**
* 准备数据
* 数据有效期为1分钟
*/
DateFormat format = new SimpleDateFormat("HH:mm");
String str = format.format(new Date()); //一分钟后str会变
Map<String,String> map = new HashMap<>();
map.put("id", id);
map.put("show5 name", "user:"+id+str);
/**
* 业务数据
*/
String data = JSON.toJSONString(map);
/**
* 对返回的内容做hash计算
*
* ETag 的问题是服务期必须要对数据做处理,这会消耗服务器的cpu
*/
String md5str = DigestUtils.md5Hex(data);
/**
* 如果第一次请求中有ETag
* 则浏览器在后续对该资源的访问的请求头里都会携带If-None-Match
* 值与服务器返回的ETag值相同
*/
String IfNoneMatch = request.getHeader("If-None-Match");
/**
* 第一次请求没有If-Modified-Since
*/
boolean doCache = true;
if(IfNoneMatch == null || IfNoneMatch.trim().equals("") || IfNoneMatch.equals("null")) {
doCache = false;
} else {
/**
* 当前时间大于缓存过期时间
*/
doCache = md5str.equals(IfNoneMatch);
}
if(doCache) {
/**
* 返回304并且 body没有内容,减少数据测传输
*/
response.setStatus(304);
return null;
}else {
response.addHeader("ETag", md5str);
return data;
}
}
总结
- Cache-Control: 浏览器直接使用本地缓存不发送http请求
- Expires:览器直接使用本地缓存不发送http请求,蛋可能会因为客户端修改本地时间后导致缓存失效
- Last-Modified/If-Modified-Since: 浏览器还是会发送网络请求,服务端返回304和空的body,减少了网络传输包的大小
- ETag/If-None-Match:浏览器还是会发送网络请求,服务端返回304和空的body,但是服务端需要计算数据hash后是否与If-None-Match一致,这里还是会消耗资源
注意测试的时候浏览器不要F5强刷,F12不要勾选禁用缓存 firefox效果比较明显