在Nginx端反向代理使用了缓存,由此引出了一个较为严重的问题,最新的文章不能及时更新,本文讨论的该问题的解决思路。
问题描述
在Nginx端,我缓存了读取指定具体文章的链接,以减免对Tomcat服务器的访问请求,链接形式如下:
location ~* /xxx/[0-9]+/[0-9]+
但是,缓存之后存在一个问题,最新的文章不能及时更新。
举个例子来说,假设文章一天一更,如果最新文章是/xxx/001/099
,
而我在文章的下部会有一个导航:上一页
、 目录
、 下一页
;
现在,
上一页
指向/xxx/001/098
,
下一页
指向/xxx/001/100
,
服务端在处理/xxx/001/100
时,直接返回目录页(章节id的生成遵循一定原理,删除、插入文章等功能暂不考虑)
缓存后,游客在读取前一天的最新文章时,因为现在很多的浏览器都有预读取功能,哪怕不点下一页
,实际也会访问使得Nginx缓存了“下一天的最新文章”,这使得文章内容实际上是个“旧的”目录页。
如果缓存时间设置较长,比如一周,那么在长达一周的时间内,游客访问的/xxx/001/100
链接都是一个老的目录页。这是我们所不期望的。
解决思路
1、减少缓存过期时间
这是一种解决方案,但过期时间设置得过小,缓存也就没啥意义;有点大,对于有追更需求,希望第一时间获取最新文章的读者来说,也是不可忍受的。
2、更改下一页
指向
上一页
指向/xxx/001/098
,
下一页
指向目录页
这个影响有所减小,但较长的一段时间内,最近几天的底部导航下一页
将会失效,直接到目录页。从目录页获得的最新目录中可以访问最新章节。
3、置灰下一页
上一页
指向/xxx/001/098
,
下一页
无指向
和2 类似,较长的一段时间内,最近几天的底部导航将会下一页
将会失效。
4、提供更新缓存接口
能较好解决缓存问题,接口最好包装一下,并加上其它逻辑。
5、分情况返回Http 状态码
Tomcat服务端在处理/xxx/001/100
时,
返回正常文章时,正常处理;
返回到目录页时,考虑将状态码作修饰,比如404 OK,而不是直接200 OK
Nginx端location对404码做特殊配置,比如只缓存两分钟
proxy_cache_valid 404 120s;
本文采取的是第五种方法,并对其做详细讨论。
实现方案
SpringBoot 中修饰HTTP Status的方法具体有以下几种:
1、@ResponseStatus
@ResponseStatus
是一个注解,可以作用在方法和类上面,如下使用(也可以配合Exception),
@RequestMapping(value = "/xxx/{bookId}/{chapterId}", method = RequestMethod.GET)
@ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR,reason="server error")
public String readChapter(@PathVariable int bookId, @PathVariable int chpterId){
return "chapter";
}
其中,ResponseStatus的reason如果设置的话,会返回报错页面,这不是我们需要的。而没有设置的话,返回的是500 OK,同时能正常渲染Thymeleaf。
但是,这里面存在一个问题,该方法就只能返回一个状态值,没法分情况讨论,配合异常使用又没法正常渲染Thymeleaf。
在纠结下去即使能成也不会“优雅”。
2、@ResponseEntity
该方法的一个样例如下,对于RestAPI来说没有问题,渲染Thymeleaf待尝试。
@RequestMapping(value = "/user", method = RequestMethod.GET)
public ResponseEntity<Map<String,Object>> getUser() throws IOException{
Map<String,Object> map = new HashMap<String,Object>();
map.put("name", "zhangsan");
return new ResponseEntity<Map<String,Object>>(map,HttpStatus.OK);
}
3、HttpServletResponse
前文已经有所提及,增加状态修改即可。
@Controller
public class SampleController {
@RequestMapping("/getUser")
public String test(HttpServletResponse response){
User user = new User();
user.setId(123);
user.setSex("male");
user.setUsername("nicelee");
String data = JSON.toJSONString(user);
try {
response.setStatus(500);
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println(data);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
本文采用第三种方法。
具体实现
如何使用HttpServletResponse,同时还返回html?
其实并不一定要使用response.getWriter()然后输入,只要修改状态码以后什么都不做,其它的表现其实依然如旧。
@RequestMapping(value = "/{bookId}/{chpterId}")
String readChapter(HttpServletResponse response, Model model, @PathVariable int bookId, @PathVariable int chpterId) {
...
if ((lChpter == null) || (lChpter.size() == 0)) {
error = "无此章节!!";
response.setStatus(400);
return "index";
}else {...}
}
最后的返回结果如下: