一、页面缓存

一般做缓存都是需要频繁被读取,变更较少的情况,会考虑做缓存。
用redis做缓存,缓存页面。比如列表页面、详情页面等。
首先缓存商品列表页。

1.1 缓存商品列表页

GoodsController中,引入redis依赖。在跳转页面的RequestMapping中,添加produces参数。
将商品列表页缓存起来需要的操作:

  • 从redis里读取缓存
  • 如果有页面,直接返回
  • 如果没有,手动渲染模板
  • 缓存到redis中,并把结果返回给输出端

1.2 修改页面跳转逻辑

添加redis依赖和thymeleafViewResolver

@Autowired
private RedisTemplate redisTemplate;@Autowired
private ThymeleafViewResolver thymeleafViewResolver;

添加ResponseBody注解,在RequestMapping中添加produce参数

/*** 跳转到商品列表页面* @author 47roro* @date 2022/4/3* @param user* @param model* @return java.lang.String**/
@RequestMapping(value = "/toList",produces = "text/html;charset=utf-8")
@ResponseBody
public String toList(Model model, User user, HttpServletRequest request, HttpServletResponse response){//redis中获取页面,如果不为空,直接返回页面ValueOperations valueOperations = redisTemplate.opsForValue();String html = (String)valueOperations.get("goodsList");if(StringUtils.hasLength(html)){return html;}model.addAttribute("user", user);model.addAttribute("goodsList", goodsService.findGoodsVo());//如果为空,手动渲染,存入redis,再返回WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());html = thymeleafViewResolver.getTemplateEngine().process("goodsList", webContext);if(StringUtils.hasLength(html)){valueOperations.set("goodsList", html, 60, TimeUnit.SECONDS);}return html;
}/*** 跳转商品详情页** @param goodsId* @return java.lang.String* @author 47roro* @date 2022/4/15**/
@RequestMapping(value = "/toDetail/{goodsId}", produces = "text/html;charset=utf-8")
@ResponseBody
public String toDetail(Model model, User user, @PathVariable Long goodsId, HttpServletRequest request, HttpServletResponse response) {//redis中获取页面,如果不为空,直接返回页面ValueOperations valueOperations = redisTemplate.opsForValue();String html = (String) valueOperations.get("goodsDetail:" + goodsId);if (StringUtils.hasLength(html)) {return html;}model.addAttribute("user", user);GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);Date startDate = goodsVo.getStartDate();Date endDate = goodsVo.getEndDate();Date nowDate = new Date();int secKillStatus = 0;int remainSeconds = 0;if (nowDate.before(startDate)) {remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) / 1000);} else if (nowDate.after(endDate)) {secKillStatus = 2;remainSeconds = -1;} else {secKillStatus = 1;}model.addAttribute("secKillStatus", secKillStatus);model.addAttribute("remainSeconds", remainSeconds);model.addAttribute("goods", goodsService.findGoodsVoByGoodsId(goodsId));//如果为空,手动渲染,存入redis,再返回WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", webContext);if (StringUtils.hasLength(html)) {valueOperations.set("goodsDetail:" + goodsId, html, 60, TimeUnit.SECONDS);}return html;
}

1.3 测试

查看缓存

二、对象缓存

在一开始登录的时候,就已经把用户信息保存到redis中了。
用户大部分时候是不更新的,基本上不设置过期时间,如果用户做了相应的变更,如修改密码,那么缓存要怎么操作呢?
最简单的方法是删除redis。

三、压力测试

优化前QPS:250左右
缓存QPS:460左右
经过缓存优化后,QPS得到了近一倍的提升。

四、商品详情页面静态化

虽然用了页面缓存,速度得到了一定的提升,但是还是存在一些问题。现在渲染出来的是完整的html,即使缓存起来了,但是发送到前端的时候,发送的是完整的html,发送的数据量还是很大的。
我们需要把前端一些不需要变化的地方静态化,即前后端分离。变动的数据单独发送就行,不变的部分静态化,减小了发送的数据量。
目前来说,我们暂时不做前后端分离,只做页面静态化,然后做一些数据的更新。

4.1 跳转接口处理

首先把后端接口进行处理,以前是直接返回一个页面,现在是返回一个公共的返回对象,返回了DetailVo对象。

/*** 跳转商品详情页,静态化** @param goodsId* @return java.lang.String* @author 47roro* @date 2022/05/04**/
@RequestMapping("/detail/{goodsId}")
@ResponseBody
public RespBean toDetail(User user, @PathVariable Long goodsId) {GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);Date startDate = goodsVo.getStartDate();Date endDate = goodsVo.getEndDate();Date nowDate = new Date();int secKillStatus = 0;int remainSeconds = 0;if (nowDate.before(startDate)) {remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) / 1000);} else if (nowDate.after(endDate)) {secKillStatus = 2;remainSeconds = -1;} else {secKillStatus = 1;}DetailVo detailVo = new DetailVo();detailVo.setUser(user);detailVo.setGoodsVo(goodsVo);detailVo.setSecKillStatus(secKillStatus);detailVo.setRemainSeconds(remainSeconds);return RespBean.success(detailVo);
}

4.2 静态页面处理

thymeleaf的模板页面转化为静态页面,通过静态页面跳转,并且通过ajax,调用接口获取静态数据,手动进行渲染。

<script>$(function () {//countDown();getDetails();});function getDetails(){var goodsId = g_getQueryString("goodsId");$.ajax({url:'/goods/detail/' + goodsId,type:'GET',success:function(data){if(data.code == 200){render(data.obj);}else{layer.msg("客户端请求出错");}},error:function(){layer.msg("客户端请求出错");}});}function render(detail){var user = detail.user;var goods = detail.goodsVo;var remainSeconds = detail.remainSeconds;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);$("#seckillPrice").text(goods.seckillPrice);$("#stockCount").text(goods.stockCount);countDown();}

4.3 结果测试

正确获得数据。

五、秒杀静态化

秒杀按钮之前是个form表单提交的形式,跳转到秒杀控制器,进行订单的创建等。

<form id="secKillForm" method="post" action="/seckill/doSeckill"><input type="hidden" name="goodsId" id="goodsId"><button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
</form>

5.1 静态秒杀处理

把原来的表单提交形式,修改为普通的按钮,通过按钮触发function doSeckill()

<button class="btn btn-primary btn-block" type="button" id="buyButton" onclick="doSeckill()">立即秒杀
<input type="hidden" name="goodsId" id="goodsId">
</button>
function doSeckill(){$.ajax({url:'/seckill/doSeckill',type:'POST',data:{goodsId:$("#goodsId").val()},success:function(data){if(data.code == 200){window.location.href="/orderDetail.htm?orderId="+data.obj.id;}else{layer.msg(data.message);}},error:function(){layer.msg("客户端请求出错");}})
}

5.2 跳转接口处理

还是进行一些超卖限购的判断,然后返回order对象。

/*** 秒杀* @author 47roro* @date 2022/4/16* @param model* @param user* @param goodsId* @return java.lang.String**/
@RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(Model model, User user, Long goodsId){if(user == null){return RespBean.error(RespBeanEnum.SESSION_ERROR);}GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if(goods.getStockCount() < 1){model.addAttribute("errmsg", RespBeanEnum.EMPT_STOCK.getMessage());return RespBean.error(RespBeanEnum.EMPT_STOCK);}//判断是否重复抢购(mybatis plus)SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));if(seckillOrder != null){model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage());return RespBean.error(RespBeanEnum.REPEAT_ERROR);}Order order = orderService.seckill(user, goods);return RespBean.success(order);
}

5.3 结果测试

其中碰到问题:无法跳转页面,返回data数据。后关闭浏览器的某些插件,就可以成功跳转。

跳转成功后,一些信息还没有进行渲染。

六、订单页面静态化

6.1 跳转接口处理

返回订单详情对象detail,控制层

/*** 跳转订单详情页面* @author 47roro* @date 2022/5/5 * @param user* @param orderId * @return com.example.seckill.vo.RespBean**/
@RequestMapping("/detail")
@ResponseBody
public RespBean detail(User user, Long orderId) {if (user == null) {return RespBean.error(RespBeanEnum.SESSION_ERROR);}OrderDetailVo detail = orderService.detail(orderId);return RespBean.success(detail);
}

service层

/*** 获取订单详情* @author 47roro* @date 2022/5/5* @param orderId* @return com.example.seckill.vo.OrderDetailVo**/
@Override
public OrderDetailVo detail(Long orderId) {if(orderId == null){throw new GlobalException(RespBeanEnum.ORDER_NOT_EXIST);}Order order = orderMapper.selectById(orderId);GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(order.getGoodsId());OrderDetailVo detail = new OrderDetailVo();detail.setOrder(order);detail.setGoodsVo(goodsVo);return detail;
}

6.2 静态页面处理

发送ajax请求,通过接口获取静态数据,进行数据渲染。

<script>$(function(){getOrderDetail();});function getOrderDetail(){var orderId = g_getQueryString("orderId");$.ajax({url:'/order/detail',type:'GET',data:{orderId:orderId},success:function(data){if(data.code == 200){render(data.obj);} else {layer.msg(data.message)}},error:function(){layer.msg("客户端请求错误")}})}function render(detail){var goods = detail.goodsVo;var order = detail.order;$("#goodsName").text(goods.goodsName);$("#goodsImg").attr("src", goods.goodsImg);$("#goodsPrice").text(goods.goodsPrice);$("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd HH:mm:ss"));var status = order.status;var statusText = "";switch(status){case 0:statusText = "待支付";break;case 1:statusText = "待发货";break;case 2:statusText = "已发货";break;case 3:statusText = "已收货";break;case 4:statusText = "已退款";break;case 5:statusText = "已完成";break;}$("#status").text(statusText);}
</script>

6.3 结果测试

七、解决库存超卖

在秒杀商品更新库存时,先判断库存数,有库存则减库存,无库存则不操作。
为用户id和商品id添加唯一索引,防止同一用户在高并发下秒杀多件商品。
但是索引只是解决了seckillorder的并发,没有解决seckillgoods数量减少的并发。
数量减少的并发是通过update操作,update操作每次会加行级排他锁,在更新的同时判断数量是否大于0。大于0才进行后续操作,等于0 则返回空,不创建新订单。这样就可以解决库存超卖的问题。

7.1 秒杀服务修改

在减库存时先判断库存是否大于0,如果等于0,则直接返回null,如果有库存则返回订单。同时,将用户id+商品id,和对应的订单存入redis。

/*** 秒杀订单具体实现* @author 47roro* @date 2022/4/16* @param user* @param goods* @return com.example.seckill.pojo.Order**/
@Transactional
@Override
public Order seckill(User user, GoodsVo goods) {//秒杀商品表减库存SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id",goods.getId()));seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);//seckillGoodsService.updateById(seckillGoods);boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().setSql("stock_count" +" = stock_count - 1").eq("goods_id", goods.getId()).gt("stock_count", 0));if(!result){return null;}//生成订单Order order = new Order();order.setUserId(user.getId());order.setGoodsId(goods.getId());order.setDeliveryAddrId(0L);order.setGoodsName(goods.getGoodsName());order.setGoodsCount(1);order.setGoodsPrice(seckillGoods.getSeckillPrice());order.setOrderChannel(1);order.setStatus(0);order.setCreateDate(new Date());orderMapper.insert(order);//生成秒杀订单SeckillOrder seckillOrder = new SeckillOrder();seckillOrder.setOrderId(order.getId());seckillOrder.setUserId(user.getId());seckillOrder.setGoodsId(goods.getId());seckillOrderService.save(seckillOrder);redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(), seckillOrder);return order;
}

7.2 控制层

在控制层进行跳转的时候,判断用户是否重复购买。从redis中查找数据。

/*** 秒杀* @author 47roro* @date 2022/4/16* @param model* @param user* @param goodsId* @return java.lang.String**/
@RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(Model model, User user, Long goodsId){if(user == null){return RespBean.error(RespBeanEnum.SESSION_ERROR);}GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if(goods.getStockCount() < 1){return RespBean.error(RespBeanEnum.EMPT_STOCK);}//判断是否重复抢购(mybatis plus)SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);if(seckillOrder != null){return RespBean.error(RespBeanEnum.REPEAT_ERROR);}Order order = orderService.seckill(user, goods);return RespBean.success(order);
}

7.3 结果测试

order 十条记录。

seckillOrder 十条记录。

seckillGoods 库存为0。

7.4 问题解决

如果在进行压力测试的时候,发现数据库里信息没有变化,可能原因是测试用的config.txt中的用户登录已失效。重新执行生成用户工具类,使这些用户重新登录,然后重新进行压力测试即可。

【学习笔记】seckill-秒杀项目--(8)页面优化相关推荐

  1. webpack入门学习笔记10 —— 在项目中使用图片资源

    1. 写在前面 在前端项目中,图片是必不可少的一种资源.在使用图片的时候,我们可以有以下几种方式: 在 .html 文件中,通过 <img src="" alt=" ...

  2. Scrapy:学习笔记(2)——Scrapy项目

    Scrapy:学习笔记(2)--Scrapy项目 1.创建项目 创建一个Scrapy项目,并将其命名为"demo" scrapy startproject demo cd demo ...

  3. 秒杀项目05-页面优化技术

    秒杀项目05-页面优化技术 1. 页面缓存+URL缓存+对象缓存 1.1 页面缓存 1.2 URL缓存 1.3 对象缓存(更细粒度的缓存) 2. 页面静态化,前后端分离 1. 常用技术AngularJ ...

  4. JavaWeb黑马旅游网-学习笔记10【项目代码】

    Java后端 学习路线 笔记汇总表[黑马程序员] JavaWeb黑马旅游网-学习笔记01[准备工作] JavaWeb黑马旅游网-学习笔记02[注册功能] JavaWeb黑马旅游网-学习笔记03[登陆和 ...

  5. CSS学习笔记--浮动元素由于浏览器页面缩小而被挤到下面的解决方法

    CSS学习笔记--浮动元素由于浏览器页面缩小而被挤到下面的解决方法 参考文章: (1)CSS学习笔记--浮动元素由于浏览器页面缩小而被挤到下面的解决方法 (2)https://www.cnblogs. ...

  6. uniapp 学习笔记二十二 购物车页面结构搭建

    uniapp 学习笔记二十二 购物车页面结构搭建 cart.vue <template><view><view class="flex padding" ...

  7. STM32学习笔记——MDK新建项目

    STM32学习笔记 KIEL新建项目过程 文章目录 STM32学习笔记 前言 一.keil新建库函数版本项目 二.注意事项 1.文件分类 2.MDK配置 总结 前言 此处使用寄存器版,进行学习.后期看 ...

  8. DSB2017项目grt123代码学习笔记一:项目基本情况

    DSB2017项目grt123代码学习笔记一:项目基本情况 Kaggle上Data Science Bowl 2017年肺结节检测比赛第一名grt123团队的算法. github地址:https:// ...

  9. PyTorch学习笔记2:nn.Module、优化器、模型的保存和加载、TensorBoard

    文章目录 一.nn.Module 1.1 nn.Module的调用 1.2 线性回归的实现 二.损失函数 三.优化器 3.1.1 SGD优化器 3.1.2 Adagrad优化器 3.2 分层学习率 3 ...

  10. kvm虚拟化学习笔记(二十一)之KVM性能优化学习笔记

    本学习笔记系列都是采用CentOS6.x操作系统,KVM虚拟机的管理也是采用virsh方式,网上的很多的文章都基于ubuntu高版本内核下,KVM的一些新的特性支持更好,本文只是记录了CentOS6. ...

最新文章

  1. Tableau必知必会之用 Page 功能创建你的动态视图
  2. 优化算法:粒子群算法,遗传算法,差分进化算法
  3. 工作组模式下专用队列(Private Queue)如何引用远程队列路径
  4. 动态修改页面Meta 标签 keywords description
  5. SAP BW系统日常维护日常工作及常见的Infopackage错误
  6. Pile 0009: Vim命令梳理
  7. iOS vuforia 学习钻研(一)
  8. ExcelToHtmlTable转换算法:将Excel转换成Html表格并展示(项目源码+详细注释+项目截图)...
  9. UVA12657 Boxes in a Line【模拟】
  10. Linux内存使用消耗高
  11. SSM第一篇 最简单的SSM框架搭建过程--SSM简单整合
  12. SDP中fmtp的使用
  13. 硬盘安装工具cgi_PE系统(U盘安装)
  14. t-SNE 高维数据可视化
  15. 2022年道路运输企业安全生产管理人员报名考试及道路运输企业安全生产管理人员模拟试题
  16. Photoshop CS6 破解安装
  17. 给学妹学弟们的看书小建议!
  18. 公安部称游街示众的执法方式严重违规
  19. python爬表情包_教你用Python来爬取表情包网站的所有表情图片
  20. 如何快速通过阿里云ACP 认证?

热门文章

  1. CasperJS 实现百度登录
  2. TiDB 社区专栏:让技术人员成为更好的读者/作家
  3. 【sql】SQL3 查找当前薪水详情以及部门编号dept_no
  4. 一个检查输入内容的 AppCompatEditText 。
  5. PHY卡 网卡区别联系
  6. 夜深人静了,我们来学学分布式锁
  7. 七年级上册计算机工作计划,七年级上册班主任工作计划
  8. 进入加密的QQ空间方法
  9. 必备技能~程序员如何提高工作效率?如何更好获得领导的认可?
  10. 【成功的忙人】在北京30套房的小哥:人一闲,就废了!