要展示一个页面时,先从缓冲中取;如果取不到,再进行渲染,然后将渲染后的结果写入缓存。
对商品列表页进行优化,修改toList()这个Controller。之前是直接返回html的文件名,让SpringBoot负责渲染。现在将代码改为:
@RequestMapping(value = "/to_list",produces = "text/html") @ResponseBody public String toList(Model model, MiaoshaUser user, HttpServletRequest request,HttpServletResponse response){ model.addAttribute("user",user); List<GoodsVo> goodsVos = goodsService.listGoodsVo(); model.addAttribute("goodsList",goodsVos); //先从缓存中取 ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); String html = opsForValue.get(GoodsKey.getGoodsList.getPrefix()); if(!StringUtils.isEmpty(html)){ return html; }else { //取不到手动渲染,使用ThymeleafViewResolver WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap()); html = thymeleafViewResolver.getTemplateEngine().process("goods_list", webContext); if(!StringUtils.isEmpty(html)){ //写入缓存 opsForValue.set(GoodsKey.getGoodsList.getPrefix(),html,GoodsKey.getGoodsList.expireSeconds(), TimeUnit.SECONDS); } return html; } }页面缓存的有效期通常是比较短的,只是为了防止短时间内大量访问。这里设置为60s。
看一看这里的页面缓存能带来多大的性能提升:
这是加入缓存之前的压测数据:
这是加入缓存之后的压测数据(5000个用户,循环访问10次):
怎么QPS还变低了
重新测试,QPS为3000,上涨50%。
与页面缓存相似,只不过是带有参数的。用来优化一下详情页面。
对象缓存是更细粒度的缓存。页面缓存适合不会经常变动信息,并且访问次数较多的页面。
其实之前就用到了对象缓存,比如在redis中存储token和用户信息,这就属于对象缓存。
这是MiaoshaService中getById()的原方法:
public MiaoshaUser getById(long id) { return miaoshaUserDao.getById(id); }非常的简单,只是从数据库中根据Id查询用户。现在我们加入对象缓存。
public MiaoshaUser getById(long id) { //取缓存 ValueOperations<String, MiaoshaUser> opsForValue = redisTemplate.opsForValue(); MiaoshaUser user = opsForValue.get(MiaoshaUserKey.getById.getPrefix() + id); if(user!=null){ return user; }else { MiaoshaUser userDaoById = miaoshaUserDao.getById(id); if(userDaoById!=null){ opsForValue.set(MiaoshaUserKey.getById.getPrefix() + id,userDaoById); } return userDaoById; } }如果要实现修改密码功能,那么步骤应该是:
先把数据存到数据库中成功后,再让缓存失效不能先操作缓存,因为如果先让缓存失效,那么在处理数据库的过程中,就有可能会被其他操作重新将旧的数据插入缓存。
直接将页面缓存到浏览器,相比于缓存到redis,效率更高。这里尝试将商品详情页静态化,动态的资源是通过接口发送的。
首先,修改Controller代码,将所有的model.addAttribute()删掉,因为这是动态的。
然后定义一个对象,作为向页面传输的值:
@Data public class GoodsDetailVo { private int miaoshaStatus = 0; private int remainSeconds = 0; private GoodsVo goods; private MiaoshaUser miaoshaUser; }然后改造Controller,不再返回页面,而是返回一个Result<GoodsDetailVo>,前端通过Ajax获取数据再渲染到页面上。
@RequestMapping("/to_detail/{goodsId}") @ResponseBody public Result<GoodsDetailVo> detail(Model model, MiaoshaUser user, @PathVariable("goodsId")long goodsId){ GoodsVo goods = goodsService.getByGoodsId(goodsId); 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; } GoodsDetailVo goodsDetailVo= new GoodsDetailVo(); goodsDetailVo.setGoods(goods); goodsDetailVo.setMiaoshaStatus(miaoshaStatus); goodsDetailVo.setRemainSeconds(remainSeconds); goodsDetailVo.setMiaoshaUser(user); return Result.success(goodsDetailVo); }对html文件做出修改,之前跳转是通过后端进行页面跳转,现在可以直接通过前端进行跳转再请求数据。
在商品列表页的跳转按钮,其href应该这样设置:
<td><a th:href="'/goods_detail.html?goodsId='+${goods.id}">详情</a></td>此时goods_detail.html应该放在static的目录下,在goods_detail.html文件中获取参数使用的是正则表达式来匹配。
<div class="panel panel-default"> <div class="panel-heading">秒杀商品详情</div> <div class="panel-body"> <span id="userTip"> 您还没有登录,请登陆后再操作<br/></span> <span>没有收货地址的提示。。。</span> </div> <table class="table" id="goodslist"> <tr> <td>商品名称</td> <td colspan="3" id="goodsName"></td> </tr> <tr> <td>商品图片</td> <td colspan="3"><img id="goodsImg" width="200" height="200" /></td> </tr> <tr> <td>秒杀开始时间</td> <td id="startTime"></td> <td > <input type="hidden" id="remainSeconds" /> <span id="miaoshaTip"></span> </td> <td> <button class="btn btn-primary btn-block" type="button" id="buyButton"οnclick="doMiaosha()">立即秒杀</button> <input type="hidden" name="goodsId" id="goodsId" /> </td> </tr> <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> </table> </div> </body> <script> 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(); } $(function(){ //countDown(); getDetail(); }); 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("客户端请求有误"); } }); } 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("秒杀已经结束"); } } </script>这就是新的页面代码,所有的动态数据都是通过Ajax异步请求来获取的。
秒杀功能页面的静态化和商品详情相似。当点击“立即秒杀”进入此方法:
function doMiaosha(){ $.ajax({ url:"/miaosha/do_miaosha", type:"POST", data:{ goodsId:$("#goodsId").val(), }, success:function(data){ if(data.code == 200){ window.location.href="/order_detail.html?orderId="+data.data.id; }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); }查看页面,观察到order_detail.html的Status Code是304.
304所表达的含义就是,客户端执行了GET请求,文件并没有变化。这时文件就可以使用本地缓存的html文件了。
但是这样客户端与服务端还是发生了一次交互,如果要客户端直接从浏览器取数据,则还需要一些配置:
#static spring.resources.add-mappings=true spring.resources.cache.period= 3600 spring.resources.chain.cache=true spring.resources.chain.enabled=true spring.resources.chain.compressed=true spring.resources.chain.html-application-cache=true spring.resources.static-locations=classpath:/static/再次查看:
可以发现,这次的code成了200,并且后面的括号(from disk cache),说明这就是从缓存获取的页面。
订单详情数据需要从Controller中请求,所以写一个Controller用来返回数据。
首先创建传输的数据OrderDetailVo:
@Data public class OrderDetailVo { private GoodsVo goods; private OrderInfo order; }然后根据传入的OrderId将数据写入Vo并返回:
@GetMapping("/detail") @ResponseBody public Result<OrderDetailVo> orderInfo(MiaoshaUser user, @RequestParam("orderId") long orderId){ if(user==null){ return Result.error(CodeMsg.SESSION_ERROR); } OrderInfo orderInfo = orderService.getOrderInfoById(orderId); if(orderInfo==null){ return Result.error(CodeMsg.ORDER_NOT_EXIST); } Long goodsId = orderInfo.getGoodsId(); GoodsVo goodsVo = goodsService.getByGoodsId(goodsId); OrderDetailVo orderDetailVo = new OrderDetailVo(); orderDetailVo.setGoods(goodsVo); orderDetailVo.setOrder(orderInfo); return Result.success(orderDetailVo); }之前在压测时发现库存会变成负数,说明大量线程并发时会发生线程安全问题。
那么怎么解决呢?
首先在数据库层面,修改SQL语句。
update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0这样就可以保证只有在库存数大于0的情况下才会减库存。
现在还有一个问题,那就是一旦一个人同时发送两个请求,同时进入了下订单的方法。那么判断逻辑根本检测不出来,这样一个人就能多次下单。
解决方法还是要通过数据库,可以利用数据库的唯一索引。将MiaoshaOrder表中的userid和orderId合成一个联合索引。
其实这里还有一个问题,老师并没有提到,那就是尽管数量不会为负数了,但是订单还是会超出数量。
JS/CSS压缩,减少流量
多个JS/CSS组合,减少连接数
现在有很多模板可以自动实现这个功能,比如淘宝的Tengine,专门用来打包的Webpack。
CDN 内容分发网络