从这一节开始就是讲如何优化秒杀的功能了。第一步考虑的是页面优化技术。
页面优化技术有:
- 页面缓存 + URL缓存 + 对象缓存
- 页面静态化,前后端分离
- 静态资源优化
- CDN优化
这篇文章先讲第一项:页面缓存 + URL + 对象缓存 具体如何实现。
页面缓存
在controller层的GoodsController中以获取商品列表的list方法举例。将原本springboot自动渲染页面改成手动渲染,把页面放在缓存里。
- 修改前的代码为:
@RequestMapping("/to_list")
public String list(Model model, HttpServletResponse response,
@CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken) {
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user = userService.getByToken(response,token);//从token中读用户信息
model.addAttribute("user", user); //将user对象和goods_list.html页面中的user“关联”
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
return "goods_list"; //返回goods_list.html
}
大致步骤是:先通过缓存中的token获取用户;再从数据库中获取商品列表;最后渲染页面。
- 修改后的代码为:
@RequestMapping(value = "/to_list", produces = "text/html")
@ResponseBody
public String list(Model model, HttpServletResponse response, HttpServletRequest request,
@CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String paramToken) {
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息
model.addAttribute("user", user); //将user对象和goods_list.html页面中的user“关联”
//取缓存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
// return "goods_list"; //返回goods_list.html,由springBoot渲染
//手动渲染
WebContext ctx = new WebContext(request, response,
request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if (!StringUtils.isEmpty(html)) { //如果html不为空,先存入redis中
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
大致步骤是:先通过缓存中的token获取用户;再从缓存中取页面返回;如果缓存中没有页面,则再去数据库中查询并添加到缓存中去,然后手动渲染页面。这里在方法体上需要加@ResponseBody。
注意:这里在缓存中存放页面的有效期不能太长,因为如果页面修改了但是缓存中没有修改会造成页面不一致,所以应该把页面设置成合理的有效期,我这里设置60s,认为用户看到1min前的页面是可以接受的。
URL缓存
个人认为URL缓存和页面缓存差别不大,可能是URL缓存的细粒度更小。这里在controller层的GoodsController中以获取商品详情列表的list方法举例。
- 修改前的代码:
@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model, @PathVariable("goodsId") long goodsId, HttpServletResponse response,
@CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken) {
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user = userService.getByToken(response,token);//从token中读用户信息
model.addAttribute("user", user);
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();//转换成毫秒值
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
if (now < startAt) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int) ((startAt - now) / 1000);
} else if (now > endAt) {//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
} else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
return "goods_detail";
}
其大致步骤为:先通过缓存中的token获取用户;再从数据库中通过商品id获取商品的详情;最后渲染商品详情页面。
- 修改后的代码
@RequestMapping(value = "/to_detail/{goodsId}", produces = "text/html")
@ResponseBody
public String detail(Model model, @PathVariable("goodsId") long goodsId, HttpServletResponse response, HttpServletRequest request,
@CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String paramToken) {
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息
model.addAttribute("user", user);
//取缓存
String html = redisService.get(GoodsKey.getGoodsDetail, "" + goodsId, String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
//手动渲染
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();//转换成毫秒值
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
if (now < startAt) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int) ((startAt - now) / 1000);
} else if (now > endAt) {//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
} else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
//return "goods_detail";
WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsDetail, "" + goodsId, html);
}
return html;
}
大致步骤为:先通过缓存中的token获取用户;再从缓存中取页面返回;若缓存中没有则需要从数据库中取,并添加到缓存中去。
对象缓存
关于对象缓存,是一种细粒度更小的缓存操作。在实际项目中, 不会大规模使用页面缓存,因为涉及到分页,一般只缓存前面1-2页。
对象缓存举例:需要用到用户信息时,可以从缓存中取出。比如已知用户id获取用户信息,本来是从数据库中取,可以到缓存中取。
另外,在redis中存入token和user对也是对象缓存。
给一个对象缓存的例子:更新用户密码
public MiaoshaUser getById(long id) {
//取缓存
MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, MiaoshaUser.class);
if (user != null) {
return user;
}
//取数据库
user = miaoshaUserDao.getById(id);
if (user != null) {
redisService.set(MiaoshaUserKey.getById, "" + id, user);
}
return user;
}
public boolean updatePassword(String token, long id, String formPass) {
//取user
MiaoshaUser user = getById(id);
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//更新数据库
MiaoshaUser toBeUpdate = new MiaoshaUser();
toBeUpdate.setId(id);
toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
miaoshaUserDao.update(toBeUpdate);
//处理缓存,对象缓存一定要更新redis,否则会发生数据不一致的情况
//这也说明了在service中调用其它的对象动作要调用service,不要调用别人的dao,
// 因为别人的service可能有缓存,而别人的dao是直接在数据库中操作
redisService.delete(MiaoshaUserKey.getById, "" + id);
user.setPassword(toBeUpdate.getPassword());
redisService.set(MiaoshaUserKey.token, token, user);
return true;
}
注:更新缓存后保证数据库中数据和缓存数据一致性。 一般可以选择淘汰缓存和更新缓存。
- 淘汰:性价比高,仅仅会涉及一次未命中,再从数据库取
- 更新:操作复杂,涉及到对应的业务逻辑。
若一定要更新,比如登陆状态下更新密码,要先更新数据库,再删除缓存、更新缓存。
最好先淘汰缓存,再更新数据库信息。(若先更新在淘汰,淘汰失败缓存中有脏数据)
JMeter压测
对商品列表页面goods_list进行压测,实际上只有一个优化:页面缓存。观察其TPS是否有提高。
先测一组优化前的数据:5000个线程;10次循环。
结果为:
因为需要从数据库中取数据,运行的特别慢。TPS:245。
优化后的结果为:
运行速度比刚才快多了,TPS:989.2。
页面缓存的作用是很明显的。
缓存分类
缓存技术是运用比较广泛的技术,它有很多种类型,从前到后整理:
- 从用户发起请求开始,可以做页面的静态化,把页面缓存在用户的浏览器端;(在后面秒杀功能(5)中实现)
- 在请求到达服务器之前,可以部署CDN节点,让请求首先访问CDN;
- 请求到达网站时,也可以加缓存,比如Nginx也可以加缓存;
- 在应用程序加页面缓存,存到redis中(本篇实现的);
- 更细粒度的对象缓存(本篇实现);
- 经历上述这些后才会到达数据库,逐步削减到达数据库的压力。需要注意的是用了缓存之后会引入数据不一致的问题,所以要做平衡。