尝试对前端页面进行相应的优化,比较典型的是缓存,一些东西可以存在浏览器身上或者redis中,提高相应速度,降低后端压力。
1. 页面缓存
这里以商品列表页面为例。
原来的商品列表页面是这样写的:
1 2 3 4 5 6 7 8 9 @RequestMapping ("to_list" )public String toList (Model model,MiaoshaUser user) { if (user == null ) return "login" ; model.addAttribute("user" ,user); List<GoodsVo> goodsVoList = goodsService.getGoodsVoList(); model.addAttribute("goodsList" ,goodsVoList); return "goods_list" ; }
给他添加页面缓存:
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 @RequestMapping (value = "to_list" ,produces = "text/html" )@ResponseBody public String toList (Model model, MiaoshaUser user, HttpServletRequest request, HttpServletResponse response) throws IOException { if (user == null ){ response.sendRedirect("/login/to_login" ); return null ; } model.addAttribute("user" ,user); String html = redisService.get(GoodsKey.getGoodsList,"" ,String.class); if (!StringUtils.isEmpty(html)){ return html; } List<GoodsVo> goodsVoList = goodsService.getGoodsVoList(); model.addAttribute("goodsList" ,goodsVoList); SpringWebContext ctx = new SpringWebContext(request,response,request.getServletContext(), request.getLocale(), model.asMap(),applicationContext); html = thymeleafViewResolver.getTemplateEngine().process("goods_list" ,ctx); if (!StringUtils.isEmpty(html)){ redisService.set(GoodsKey.getGoodsList,"" ,html); } return html; }
对于商品详情页面的缓存,原来是这样写的:
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 @RequestMapping ("/to_detail/{goodsId}" )public String toDetail (@PathVariable("goodsId" ) long goodsId,Model model, MiaoshaUser user) { if (user == null ) return "login" ; model.addAttribute("user" ,user); GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); model.addAttribute("goods" ,goodsVo); long startAt = goodsVo.getStartDate().getTime(); long endAt = goodsVo.getEndDate().getTime(); long now = System.currentTimeMillis(); int miaoshaStatus = 0 ; int remainSeconds = 0 ; if (now < startAt){ miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START; remainSeconds = (int )(startAt-now)/1000 ; }else if (now > endAt){ miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA; remainSeconds = -1 ; }else { miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA; remainSeconds = 0 ; } model.addAttribute("miaoshaStatus" ,miaoshaStatus); model.addAttribute("remainSeconds" ,remainSeconds); return "goods_detail" ; }
现在改为如下,以goodsid
作为区别:
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 43 44 45 46 47 @RequestMapping (value = "/to_detail/{goodsId}" ,produces = "text/html" )@ResponseBody public String toDetail (@PathVariable("goodsId" ) long goodsId,Model model, MiaoshaUser user, HttpServletRequest request, HttpServletResponse response) throws IOException { if (user == null ){ response.sendRedirect("/login/to_login" ); return null ; } model.addAttribute("user" ,user); String html = redisService.get(GoodsKey.getGoodsDetail,"" +goodsId,String.class); if (!StringUtils.isEmpty(html)){ return html; } GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); model.addAttribute("goods" ,goodsVo); long startAt = goodsVo.getStartDate().getTime(); long endAt = goodsVo.getEndDate().getTime(); long now = System.currentTimeMillis(); int miaoshaStatus = 0 ; int remainSeconds = 0 ; if (now < startAt){ miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START; remainSeconds = (int )(startAt-now)/1000 ; }else if (now > endAt){ miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA; remainSeconds = -1 ; }else { miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA; remainSeconds = 0 ; } model.addAttribute("miaoshaStatus" ,miaoshaStatus); model.addAttribute("remainSeconds" ,remainSeconds); SpringWebContext ctx = new SpringWebContext(request,response,request.getServletContext(), request.getLocale(), model.asMap(),applicationContext); html = thymeleafViewResolver.getTemplateEngine().process("goods_detail" ,ctx); if (!StringUtils.isEmpty(html)){ redisService.set(GoodsKey.getGoodsDetail,"" +goodsId,html); } return html; } }
2. 对象缓存
就是对一个对象进行缓存,比如这里可以对MiaoshaUser
这个对象进行缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 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,"" +user.getId(),user); } return user; }
这个逻辑是十分清晰的,但是如果我是更新一个信息呢?比如更新登录的用户的Nickname
。那么就要注意,先更新数据库,在更新好数据库之后,一定要注意处理相关的缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean updateUsername (String token,long id,String newUsername) { MiaoshaUser user = getById(id); if (user == null ) throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST); MiaoshaUser toBeUpdate = new MiaoshaUser(); toBeUpdate.setId(id); toBeUpdate.setNickname(newUsername); miaoshaUserDao.update(toBeUpdate); redisService.del(MiaoshaUserKey.getById,"" +id); user.setNickname(newUsername); redisService.set(MiaoshaUserKey.token,token,user); return true ; }
3. 商品详情页面静态化
之前我们队商品详情页面进行了redis
缓存,因为这个接口只是展示相应产品详情和秒杀倒计时等信息,只要显示几个关键信息即可,其他的都可以进行静态化。
这种技术,我们其实已经做过了,在之前的电商项目中,前端用vue.js
等其他js框架或者不用框架,直接jquery
。前端分为两部分,一部分是不改变的html
块,还有一块就是数据,他只要后端传数据到前端即可,用到ajax
技术。
确定哪些是需要传到前端的数据:
1 2 3 4 5 6 7 @Data public class DetailVo { private int miaoshaStatus = 0 ; private int remainSeconds = 0 ; private GoodsVo goods; private MiaoshaUser user; }
将detail
这个接口改为:
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 @RequestMapping (value = "/detail/{goodsId}" )@ResponseBody public Result<DetailVo> toDetail (@PathVariable("goodsId" ) long goodsId, MiaoshaUser user, HttpServletRequest request, HttpServletResponse response) throws IOException { if (user == null ){ response.sendRedirect("/login/to_login" ); return null ; } GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); long startAt = goodsVo.getStartDate().getTime(); long endAt = goodsVo.getEndDate().getTime(); long now = System.currentTimeMillis(); int miaoshaStatus = 0 ; int remainSeconds = 0 ; if (now < startAt){ miaoshaStatus = Constants.MiaoshaStatus.BEFORE_START; remainSeconds = (int )(startAt-now)/1000 ; }else if (now > endAt){ miaoshaStatus = Constants.MiaoshaStatus.AFTER_MIAOSHA; remainSeconds = -1 ; }else { miaoshaStatus = Constants.MiaoshaStatus.ON_MIAOSHA; remainSeconds = 0 ; } DetailVo detailVo = new DetailVo(); detailVo.setUser(user); detailVo.setGoods(goodsVo); detailVo.setMiaoshaStatus(miaoshaStatus); detailVo.setRemainSeconds(remainSeconds); return Result.success(detailVo); }
后端的数据已经有了,那么前端只要接收这些数据即可。
首先是在static
目录下新建goods_detail.htm
页面,里面讲themeleaf
的动态获取的对象全部去除。改为最普通的html
,只要用id
来标识一下,然后在js
中赋值即可。比如:
1 2 3 4 5 6 7 8 9 10 11 12 <tr > <td > 商品原价</td > <td colspan ="3" id ="goodsPrice" > </td > </tr > <tr > <td > 秒杀价</td > <td colspan ="3" id ="miaoshaPrice" > </td > </tr > <tr > <td > 库存数量</td > <td colspan ="3" id ="stockCount" > </td > </tr >
js
部分,首先是打开页面就执行这个方法:
1 2 3 4 $(function ( ) { getDetail(); });
里面的getDetail
方法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function getDetail ( ) { var goodsId = g_getQueryString("goodsId" ); $.ajax({ url:"/goods/detail/" +goodsId, type:"GET" , success:function (data ) { if (data.code == 0 ){ render(data.data); }else { layer.msg(data.msg); } }, error:function ( ) { layer.msg("客户端请求有误" ); } }); }
获取goods_id
,因为list
页面的商品详情请求是
1 <td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td>
所以下面要获取这个参数:
1 2 3 4 5 6 function g_getQueryString (name ) { var reg = new RegExp ("(^|&)" + name + "=([^&]*)(&|$)" ); var r = window .location.search.substr(1 ).match(reg);if (r != null ) return unescape (r[2 ]);return null ;};
获取到之后就请求后端接口,获取数据去渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function render (detail ) { var miaoshaStatus = detail.miaoshaStatus; var remainSeconds = detail.remainSeconds; var goods = detail.goods; var user = detail.user; if (user){ $("#userTip" ).hide(); } $("#goodsName" ).text(goods.goodsName); $("#goodsImg" ).attr("src" , goods.goodsImg); $("#startTime" ).text(new Date (goods.startDate).format("yyyy-MM-dd hh:mm:ss" )); $("#remainSeconds" ).val(remainSeconds); $("#goodsId" ).val(goods.id); $("#goodsPrice" ).text(goods.goodsPrice); $("#miaoshaPrice" ).text(goods.miaoshaPrice); $("#stockCount" ).text(goods.stockCount); countDown(); }
倒计时countDown()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function countDown ( ) { var remainSeconds = $("#remainSeconds" ).val(); var timeout; if (remainSeconds > 0 ){ $("#buyButton" ).attr("disabled" , true ); $("#miaoshaTip" ).html("秒杀倒计时:" +remainSeconds+"秒" ); timeout = setTimeout(function ( ) { $("#countDown" ).text(remainSeconds - 1 ); $("#remainSeconds" ).val(remainSeconds - 1 ); countDown(); },1000 ); }else if (remainSeconds == 0 ){ $("#buyButton" ).attr("disabled" , false ); if (timeout){ clearTimeout(timeout); } $("#miaoshaTip" ).html("秒杀进行中" ); }else { $("#buyButton" ).attr("disabled" , true ); $("#miaoshaTip" ).html("秒杀已经结束" ); } }
上面的日期格式化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Date .prototype.format = function (format ) { var args = { "M+" : this .getMonth() + 1 , "d+" : this .getDate(), "h+" : this .getHours(), "m+" : this .getMinutes(), "s+" : this .getSeconds(), }; if (/(y+)/ .test(format)) format = format.replace(RegExp .$1 , (this .getFullYear() + "" ).substr(4 - RegExp .$1. length)); for (var i in args) { var n = args[i]; if (new RegExp ("(" + i + ")" ).test(format)) format = format.replace(RegExp .$1 , RegExp .$1. length == 1 ? n : ("00" + n).substr(("" + n).length)); } return format; };
4. 订单详情页面静态化
之前的do_miaosha
要进行修改,不能再返回String
了,而是要返回Json
数据,原来是这样写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RequestMapping ("/do_miaosha" )public String do_miaosha (Model model, MiaoshaUser user, @RequestParam("goodsId" ) long goodsId) { if (user == null ) return "login" ; model.addAttribute("user" ,user); GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); if (goodsVo.getStockCount() <= 0 ){ model.addAttribute("errmsg" , CodeMsg.MIAO_SHA_OVER.getMsg()); return "miaosha_fail" ; } MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId); if (miaoshaOrder != null ){ model.addAttribute("errmsg" , CodeMsg.REPEATE_MIAOSHA.getMsg()); return "miaosha_fail" ; } OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo); model.addAttribute("orderInfo" , orderInfo); model.addAttribute("goods" , goodsVo); return "order_detail" ; }
现在改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RequestMapping (value = "/do_miaosha" ,method = RequestMethod.POST)@ResponseBody public Result<OrderInfo> do_miaosha (Model model, MiaoshaUser user, @RequestParam("goodsId" ) long goodsId) { if (user == null ) return Result.error(CodeMsg.SESSION_ERROR); GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); if (goodsVo.getStockCount() <= 0 ){ return Result.error(CodeMsg.MIAO_SHA_OVER); } MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId); if (miaoshaOrder != null ){ return Result.error(CodeMsg.REPEATE_MIAOSHA); } OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo); return Result.success(orderInfo); }
下面按秒杀按钮:
1 2 3 4 <td > <button class ="btn btn-primary btn-block" type ="button" id ="buyButton" onclick ="doMiaosha()" > 立即秒杀</button > <input type ="hidden" name ="goodsId" id ="goodsId" /> </td >
下面进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function doMiaosha ( ) { $.ajax({ url:"/miaosha/do_miaosha" , type:"POST" , data:{ goodsId:$("#goodsId" ).val(), }, success:function (data ) { if (data.code == 0 ){ window .location.href="/order_detail.htm?orderId=" +data.data.id; }else { layer.msg(data.msg); } }, error:function ( ) { layer.msg("客户端请求有误" ); } }); }
一旦抢到商品,那么就跳转到订单详情页面,order_detail.htm
中的处理与上面的一样:
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 function render (detail ) { var goods = detail.goods; var order = detail.order; $("#goodsName" ).text(goods.goodsName); $("#goodsImg" ).attr("src" , goods.goodsImg); $("#orderPrice" ).text(order.goodsPrice); $("#createDate" ).text(new Date (order.createDate).format("yyyy-MM-dd hh:mm:ss" )); var status = "" ; if (order.status == 0 ){ status = "未支付" }else if (order.status == 1 ){ status = "待发货" ; } $("#orderStatus" ).text(status); } $(function ( ) { getOrderDetail(); }) function getOrderDetail ( ) { var orderId = g_getQueryString("orderId" ); $.ajax({ url:"/order/detail" , type:"GET" , data:{ orderId:orderId }, success:function (data ) { if (data.code == 0 ){ render(data.data); }else { layer.msg(data.msg); } }, error:function ( ) { layer.msg("客户端请求有误" ); } }); }
要显示order_detail
,他请求/order/detail
这个接口,需要order
和goods
两个对象,所以新建一个vo
:
1 2 3 4 5 @Data public class OrderDetailVo { private GoodsVo goods; private OrderInfo order; }
对OrderController
增加接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RequestMapping ("/detail" )@ResponseBody public Result<OrderDetailVo> info (MiaoshaUser user, @RequestParam("orderId" ) long orderId) { if (user == null ) { return Result.error(CodeMsg.SESSION_ERROR); } OrderInfo order = orderService.getOrderById(orderId); if (order == null ) { return Result.error(CodeMsg.ORDER_NOT_EXIST); } long goodsId = order.getGoodsId(); GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); OrderDetailVo vo = new OrderDetailVo(); vo.setOrder(order); vo.setGoods(goods); return Result.success(vo); }
这样就ok了,对于商品详情和订单详情两个页面完成了静态化。
5. 页面缓存
Cache-Control
:指定缓存有多少时间
为了在浏览器端进行缓存,以及控制缓存时间,这里可以添加一些配置:
1 2 3 4 5 6 7 8 9 10 spring: resources: static -locations: classpath:/static / add-mappings: true cache-period: 3600 chain: cache: true enabled: true gzipped: true html-application-cache: true
6. 解决超卖
先解决卖成负数的问题:
在reduceStock(MiaoshaGoods g);
这个方法里,sql
要多加一个stock_count > 0
即:
1 update miaosha_goods set stock_count = stock_count-1 where goods_id=#{goodsId} and stock_count > 0
给miaosha_order
中的user_id
和goods_id
建立唯一联合索引。保证同一个人不能秒杀都两个商品。
但是从压测结果来看,虽然解决了上面两个问题。但是仍然发生了超卖现象,即比如只有10件秒杀商品,但是有22个人抢到了。
— 2019/4/17号补充
关于这里的超卖问题,视频中的解决是用数据库的锁来实现,但是这样的话显然效率会比较低,我觉得可以交给redis+MQ来解决,redis结合lua脚本可以实现原子性,这样子可以保证redis中库存扣减不会出问题,再结合MQ的队列,可以避免高并发下发生库存扣减错误问题。这个在mama-buy这个项目中进行了一些演示和说明。
7. 静态资源优化
js/css压缩
多个js/css组合,减少连接数
CDN就近访问
nginx加缓存,页面缓存,对象缓存