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

1. 页面缓存+URL缓存+对象缓存

1.1 页面缓存

页面缓存就是将请求访问的页面放到redis里保存,这种缓存技术一般用于不会经常变动信息,并且访问次数较多的页面,这样就不用每次都动态加载
商品列表页 页面缓存: 1 取缓存 2 手动渲染 3 结果输出

  1. 修改GoodsController中的/goods/to_list/请求返回的内容,使其返回html页面的内容。
	@RequestMapping(value = "/to_list", produces = "text/html")
    @ResponseBody
    public String toList(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser 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";
        WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
        //手动渲染
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
        if (StringUtils.isNotBlank(html)) {
            redisService.set(GoodsKey.getGoodsList, "", html);
        }
        return html;
    }
  1. 编写GoodsKey(存储redis中的key和expireTime)
public class GoodsKey extends BasePrefix{

    private GoodsKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }

    public static GoodsKey getGoodsList = new GoodsKey(60, "gl");

}

1.2 URL缓存

这里的URL缓存相当于页面缓存,只是针对详情页/goods/to_detail/{goodsId}
不同的详情页 显示不同缓存页面+渲染 实质一样

1.3 对象缓存(更细粒度的缓存)

对象缓存就是将对象放到缓存中
MiaoshaUserservice.java

/**
     * 将从数据库中获取对象,优化为从缓存中获取对象数据
     * 先从缓存中取用户数据,取不到,再从数据库中取,并且将数据放到缓存一份
     * @param id
     * @return
     */
    public MiaoshaUser getById(long id) {
        //取缓存
        MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, MiaoshaUser.class);
        if (user != null) {
            return user;
        }
        //取数据库
        user = miaoshaUserMapper.selectById(id);
        if (user != null) {
            redisService.set(MiaoshaUserKey.getById, "" + id, user);
        }
        return user;
    }
/**
     * 更新用户密码: 涉及到对象缓存 -- 若更新对象缓存的相关数据 要处理缓存
     * 同步数据库和redis缓存的信息,否则会造成数据不一致的情况
     */
    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()));
        miaoshaUserMapper.update(toBeUpdate);
        //处理缓存,防止数据库中与redis缓存中的数据不一致,所有涉及到该对象的缓存都要处理
        redisService.delete(MiaoshaUserKey.getById, "" + id);
        user.setPassword(toBeUpdate.getPassword());
        redisService.set(MiaoshaUserKey.token, token, user);
        return true;
    }

2. 页面静态化,前后端分离

1. 常用技术AngularJS、Vue.js

2. 优点: 利用浏览器的缓存

3. 商品详情页面静态化(将页面与动态的内容分离)

修改Controller层的方法

GoodsController.java

	@RequestMapping(value = "/detail/{goodsId}")
    @ResponseBody
    public Result<GoodsDetailVo> detail(Model model, MiaoshaUser user, @PathVariable("goodsId") long goodsId) {

        GoodsVo goods = goodsService.getGoodsVoByGoodsId(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 vo = new GoodsDetailVo();
        vo.setGoods(goods);
        vo.setUser(user);
        vo.setRemainSeconds(remainSeconds);
        vo.setMiaoshaStatus(miaoshaStatus);
        return Result.success(vo);
    }

添加Vo对象

GoodsDetailVo.java

@Data
public class GoodsDetailVo {

    private int miaoshaStatus = 0;
    private int remainSeconds = 0;
    private GoodsVo goods;
    private MiaoshaUser user;
}

修改common.js添加时间格式化函数和获取url参数的函数

//展示loading
function g_showLoading(){
	var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;  
	return idx;
}
//salt
var g_passsword_salt="hmxP@ssw0rd"
// 获取url参数
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;
};
//设定时间格式化函数,使用new Date().format("yyyyMMddhhmmss");
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;
};

将商品详情页面放到/resources/static目录下

在这里插入图片描述
并修改其内容,使其不再依赖Thymeleaf模板引擎
goods_detail.html

<!DOCTYPE HTML>
<html>
<head>
    <title>商品详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js}"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
    <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- md5.js -->
    <script type="text/javascript" src="/js/md5.min.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>

<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>
        	<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
        		<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
        		<input type="hidden" name="goodsId" id="goodsId"/>
        	</form>
        </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(){
	//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 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(){
    //debugger;
	var remainSeconds = $("#remainSeconds").val();
	var timeout;
	//debugger;
	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>
</html>

修改goods_list页面的部分内容

在这里插入图片描述

4. 订单详情页面静态化

新增CodeMsg

	//订单模块 5004XX
    ORDER_NOT_EXIST(500400, "订单不存在"),

修改Controller层

MiaoshaController.java

@RequestMapping("/miaosha")
@Controller
public class MiaoshaController {

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private MiaoshaService miaoshaService;

    /**
     *  QPS:909.3
     *  异常%: 4.38%
     *  5000 * 10
     */
    /**
     * GET POST有什么区别?
     * GET幂等 <a href="/delete?id=1212">
     */
    @RequestMapping(value = "/do_miaosha", method= RequestMethod.POST)
    @ResponseBody
    public Result<OrderInfo> doMiaosha(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId) {
        model.addAttribute("user", user);
        if (user == null) {
            return Result.fail(CodeMsg.SESSION_ERROR);
        }
        // 判断库存
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goods.getStockCount();
        if (stock <= 0) {
            return Result.fail(CodeMsg.MIAO_SHA_OVER);
        }
        // 判断是否已经秒杀到了
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            return Result.fail(CodeMsg.REPEAT_MIAOSHA);
        }
        // 减库存 下订单 写入秒杀订单
        OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
        return Result.success(orderInfo);
    }

}

添加vo对象

OrderDetailVo.java

@Data
public class OrderDetailVo {
    private GoodsVo goods;
    private OrderInfo order;
}

将订单详情页面放到/resources/static目录下

在这里插入图片描述
order_detail.html

<!DOCTYPE HTML>
<html>
<head>
    <title>订单详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
    <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
    <!-- layer -->
    <script type="text/javascript" src="/layer/layer.js"></script>
    <!-- md5.js -->
    <script type="text/javascript" src="/js/md5.min.js"></script>
    <!-- common.js -->
    <script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
  <div class="panel-heading">秒杀订单详情</div>
  <table class="table" id="goodslist">
        <tr>  
        <td>商品名称</td>  
        <td id="goodsName" colspan="3"></td>
     </tr>  
     <tr>  
        <td>商品图片</td>  
        <td colspan="2"><img id="goodsImg" wid="200" height="200" /></td>
     </tr>
      <tr>  
        <td>订单价格</td>  
        <td colspan="2" id="goodsPrice"></td>
     </tr>
     <tr>
     		<td>下单时间</td>  
        	<td id="createDate" colspan="2"></td>
     </tr>
     <tr>
     	<td>订单状态</td>  
        <td id="orderStatus">
        </td>  
        <td>
        	<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
        </td>
     </tr>
      <tr>
     		<td>收货人</td>  
        	<td colspan="2">XXX  18812341234</td>  
     </tr>
     <tr>
     		<td>收货地址</td>  
        	<td colspan="2">北京市昌平区回龙观龙博一区</td>  
     </tr>
  </table>
</div>

</body>
</html>

<script>
    $(function(){
        getOrderDetail();
    });

    function getOrderDetail() {
        var orderId = g_getQueryString("orderId")
        $.ajax({
            url: "/order/detail/",
            type: "GET",
            data: {
                orderId:orderId
            },
            success:function(data){
                debugger;
                if (data.code === 0) {
                    render(data.data);
                } else {
                    layer.msg(data.msg);
                }
            },
            error:function() {
                layer.msg("客户端请求有误");
            }
        })
    }

    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);
    }
</script>

修改商品详情页的部分内容

在这里插入图片描述
good_detail.html

//秒杀
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.html?orderId="+data.data.id;
            } else {
                layer.msg(data.msg);
            }
        },
        error:function() {
            layer.msg("客户端请求有误");
        }
    })
}

添加静态资源配置

# static
# SPRING RESOURCES HANDLING (ResourceProperties)
# 是否启用默认资源处理
spring.web.resources.add-mappings=true
# 资源处理程序服务的资源的缓存周期。如果未指定持续时间后缀,则将使用秒。可以被 'spring.web.resources.cache.cachecontrol' 属性覆盖。
spring.web.resources.cache.period=3600
# 是否启用资源链中的缓存。
spring.web.resources.chain.cache=true
# 是否启用 Spring 资源处理链。默认情况下,除非至少启用了一种策略,否则禁用。
spring.web.resources.chain.enabled=true
# 是否启用已压缩资源的解析(gzip、brotli)。检查具有“.gz”或“.br”文件扩展名的资源名称。
spring.web.resources.chain.compressed=false
# 静态资源的位置。默认为类路径:classpath:/static/
spring.web.resources.static-locations=classpath:/static/

浏览器缓存

在这里插入图片描述

解决一个bug

修改OrderService中的创建订单方法
在这里插入图片描述

一些问题

解决超卖问题

当高并发访问秒杀请求时,会出现超卖问题,我们可以通过在数据库减库存操作时,在SQL语句上加上stockCount>0的判断
在这里插入图片描述

一个人秒杀多次的问题

  1. 先给miaosha_order表的userid和goodsid添加唯一索引
    在这里插入图片描述

当进入秒杀业务的人数>库存数时,要判断当前用户是否减库存成功,成功则生成订单,失败抛出异常

MiaoshaService.java

@Transactional
    public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
        // 减库存 下订单 写入秒杀订单
        int row = goodsService.reduceStock(goods);
        if (row > 0) {
            // order_info miaosha_order
            return orderService.createOrder(user, goods);
        }
        //row == 0 说明减库存失败,订单也不应该创建,报错回滚
        throw new GlobalException(CodeMsg.MIAO_SHA_OVER);
    }

优化

将miaoshaoOrder信息作为对象缓存缓存到redis中

添加OrderKey

OrderKey.java

public class OrderKey extends BasePrefix {

	public OrderKey(String prefix) {
		super(prefix);
	}
	public static OrderKey getMiaoshaOrderByUidGid = new OrderKey("moug");
}

修改OrderService

OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RedisService redisService;

    public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
        //return orderMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
        return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
    }

	@Transactional
    public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCreateDate(new Date());
        orderInfo.setDeliveryAddrId(0L);
        orderInfo.setGoodsCount(1);
        orderInfo.setGoodsId(goods.getId());
        orderInfo.setGoodsName(goods.getGoodsName());
        orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
        orderInfo.setOrderChannel(1);
        orderInfo.setStatus(1);
        orderInfo.setStatus(0);
        orderInfo.setUserId(user.getId());
        //insert方法会把自增的主键设置到对象属性中
        orderMapper.insert(orderInfo);
        MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
        miaoshaOrder.setGoodsId(goods.getId());
        miaoshaOrder.setOrderId(orderInfo.getId());
        miaoshaOrder.setUserId(user.getId());
        orderMapper.insertMiaoshaOrder(miaoshaOrder);
        redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);
        return orderInfo;
    }

    public OrderInfo getOrderById(long orderId) {
        return orderMapper.selectById(orderId);
    }
}

重新压测

先启动MyUtil类的主方法,让5000个用户的token被保存在redis中.

将jar包放到centos中并运行,然后开始压测,并查看生成的订单信息。在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3. 静态资源优化

3.1 JS/CSS压缩,减少流量

3.2 多个JS/CSS组合,减少连接数

3.3 CDN就近访问

4. CDN优化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值