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

一、页面缓存

一般做缓存都是需要频繁被读取,变更较少的情况,会考虑做缓存。
用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中的用户登录已失效。重新执行生成用户工具类,使这些用户重新登录,然后重新进行压力测试即可。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值