欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

秒杀项目学习第五章

程序员文章站 2024-03-01 19:24:40
...

主要内容

  1. 页面缓存+URL缓存+对象缓存
  2. 页面静态化,前后端分离
  3. 静态资源优化
  4. CDN优化

一、 页面缓存+URL缓存+对象缓存

页面缓存

  1. 取缓存
  2. 手动渲染模板
  3. 结果输出直接返回html页面

以获取商品列表页面为例

1.原先controller
秒杀项目学习第五章2.现在controller

@RequestMapping(value="/to_list", produces="text/html")
	@ResponseBody
	public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
		model.addAttribute("user", user);
		//取缓存见3
		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.isEmpty(html)) {
			redisService.set(GoodsKey.getGoodsList, "", html);
		}
		return html;
	}

2.GoodsKey
一般页面缓存时间都比较短,时间长的话页面及时性就不会很好
秒杀项目学习第五章3.Redis中存的数据
秒杀项目学习第五章4.浏览器中的显示
秒杀项目学习第五章

URL缓存

以商品详情页面为例

1.以前的controller

@RequestMapping("/to_detail/{goodsId}")
    public String detail(Model model,MiaoshaUser user,
    		@PathVariable("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
    	model.addAttribute("goods", goods);
    	
    	long startAt = goods.getStartDate().getTime();
    	long endAt = goods.getEndDate().getTime();
    	long now = System.currentTimeMillis();
    	
    	int miaoshaStatus = 0;
    	int remainSeconds = 0;
    	int endSeconds = 0;
    	if(now < startAt ) {//秒杀还没开始,倒计时
    		miaoshaStatus = 0;
    		remainSeconds = (int)((startAt - now )/1000);
    		endSeconds = (int)((endAt - now )/1000);
    	}else  if(now > endAt){//秒杀已经结束
    		miaoshaStatus = 2;
    		remainSeconds = -1;
    	}else {//秒杀进行中
    		miaoshaStatus = 1;
    		remainSeconds = 0;
			endSeconds = (int)((endAt - now )/1000);
    	}
    	model.addAttribute("miaoshaStatus", miaoshaStatus);
    	model.addAttribute("remainSeconds", remainSeconds);
    	model.addAttribute("endSeconds", endSeconds);

        return "goods_detail";
    }

2.现在的controller

@RequestMapping(path = "/to_detail/{goodsid}",produces = "text/html")
	@ResponseBody
	public String toDetail(HttpServletRequest request,
						   HttpServletResponse response,
						   Model model,
						   MiaoshaUser user,
						   @PathVariable("goodsid")long goodsid) {
    	model.addAttribute("user", user);
		//取缓存
		String html = redisService.get(GoodsKey.getGoodsDetail,""+goodsid,String.class);
		if(!StringUtils.isEmpty(html)){
			return html;
		}
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsid);
    	model.addAttribute("goods", goods);
    	
    	long startAt = goods.getStartDate().getTime();
    	long endAt = goods.getEndDate().getTime();
    	long now = System.currentTimeMillis();
    	
    	int miaoshaStatus = 0;
    	int remainSeconds = 0;
    	int endSeconds = 0;
    	if(now < startAt ) {//秒杀还没开始,倒计时
    		miaoshaStatus = 0;
    		remainSeconds = (int)((startAt - now )/1000);
    		endSeconds = (int)((endAt - now )/1000);
    	}else  if(now > endAt){//秒杀已经结束
    		miaoshaStatus = 2;
    		remainSeconds = -1;
    	}else {//秒杀进行中
    		miaoshaStatus = 1;
    		remainSeconds = 0;
			endSeconds = (int)((endAt - now )/1000);
    	}
    	model.addAttribute("miaoshaStatus", miaoshaStatus);
    	model.addAttribute("remainSeconds", remainSeconds);
    	model.addAttribute("endSeconds", endSeconds);
//手动渲染
		WebContext context = new WebContext(request,
				response,
				request.getServletContext(),
				request.getLocale(),
				model.asMap());
		html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", context);
		if(!StringUtils.isEmpty(html)){
			//System.out.println(html);
			redisService.set(GoodsKey.getGoodsDetail,""+goodsid,html);
		}
		return html;
      //  return "goods_detail";
    }

和以前controller的区别

1.取缓存
秒杀项目学习第五章
2.渲染模板直接返回html
秒杀项目学习第五章
3.Redis中数据
秒杀项目学习第五章

和页面缓存的区别

  1. 页面缓存不存在随着url的变化页面变化的情况
  2. URL缓存虽然模板一样但是渲染的数据不一样,存到redis中时的key是不同的

对象缓存

更细粒度的缓存以前已经用到了,在分布式Session中我们把token映射成User时就是对象级缓存
秒杀项目学习第五章其他的类似用到数据库查询根据某值(token)取某值(User)的也可以这么改造

比如:MiaoshaUserService.getById方法
1.设置一个key
一般对象缓存都是永久有效
秒杀项目学习第五章2.以前的getByid方法
秒杀项目学习第五章
2.修改后的getByid方法
秒杀项目学习第五章注意:当更新已经缓存的对象时,一般步骤如下

  1. 取出要更新的对象
  2. 更新数据库
  3. 更新所有与此对象有关的缓存

对象缓存也说明了为啥Aservice使用Bservice中的功能时为啥不能直接用Bdao因为Bservice中可能存在缓存操作,调Bservice可以提高效率

二、页面静态化,前后端分离

  1. 常用技术AngularJS、Vue.js
  2. 优点:利用浏览器的缓存

商品详情页面静态化

1.GoodsController.detail方法

//页面静态化
	@RequestMapping(path = "/to_detail/{goodsid}")
	@ResponseBody
	public Result<GoodsDetailVo> detail(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;
		int endSeconds = 0;
		if(now < startAt ) {//秒杀还没开始,倒计时
			miaoshaStatus = 0;
			remainSeconds = (int)((startAt - now )/1000);
			endSeconds = (int)((endAt - now )/1000);
		}else  if(now > endAt){//秒杀已经结束
			miaoshaStatus = 2;
			remainSeconds = -1;
		}else {//秒杀进行中
			miaoshaStatus = 1;
			remainSeconds = 0;
			endSeconds = (int)((endAt - now )/1000);
		}
		//把以前放到model里的对象封装进GoodsDetailVo,见2
		GoodsDetailVo vo = new GoodsDetailVo();
		vo.setGoods(goods);
		vo.setMiaoshaStatus(miaoshaStatus);
		vo.setRemainSeconds(remainSeconds);
		vo.setEndSeconds(endSeconds);
		vo.setUser(user);
		return Result.success(vo);
		//  return "goods_detail";
	}

2.GoodsDetailVo

/**
 * 向页面传数据
 */
public class GoodsDetailVo {
    private int miaoshaStatus = 0;
    private int remainSeconds = 0;
    private int endSeconds = 0;
    private GoodsVo goods;
    private MiaoshaUser user;

3.修改goods_detail页面

  • 把goods_detail.html页面改名为goods_detail.htm放到static目录下去掉Thymeleaf的所有东西

秒杀项目学习第五章

改名原因是application.properties中配置了如下参数
spring.thymeleaf.prefix=classpath:/templates/	
spring.thymeleaf.suffix=.html
  • 修改goods_list.html中的跳转地址,直接跳转到页面
    秒杀项目学习第五章
  • 修改good_detail.htm中的代码

秒杀项目学习第五章

  • js代码,ajax请求json数据后填入html中
/*这是jQuery的写法
    * 原本为:
    *
    *    $(document).ready(function(){
            // 开始写 jQuery 代码...
         });
         * 页面加载完之后才会执行
    * */
    $(function(){

        getDetail();
        //countDown();
    });

    function getDetail() {
        //获取参数值
        var goodsId = g_getQueryString("goodsId");
        $.ajax({
            url:"/goods/to_detail/"+goodsId,
            type:"GET",
            success:function(data){
                //如果Result中code==0那么就是成功的
                if(data.code==0){
                    //渲染页面的方法
                    render(data.data);
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function (){
                layer.msg("客户端请求错误333");
            }
        });
    }
    function render(vo) {
        var miaoshaStatus = vo.miaoshaStatus;
        var remainSeconds = vo.remainSeconds;
        var endSeconds = vo.endSeconds;
        var goods = vo.goods;
        var user = vo.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);
        $("#endSeconds").val(endSeconds);
        $("#goodsId").val(goods.id);
        $("#goodsPrice").text(goods.goodsPrice);
        $("#miaoshaPrice").text(goods.miaoshaPrice);
        $("#stockCount").text(goods.stockCount);
        //处理id=miaoshaTip
        countDown();
    }
    function countDown(){
        var remainSeconds = $("#remainSeconds").val();
        var endSeconds = $("#endSeconds").val();
        var timeout;
        var endtimeout;
        if(remainSeconds > 0){//秒杀还没开始,倒计时
            $("#buyButton").attr("disabled", true);
            timeout = setTimeout(function(){
                $("#miaoshaTip").html("剩余时间:"+(remainSeconds - 1));
                $("#remainSeconds").val(remainSeconds - 1);
                $("#endSeconds").val(endSeconds - 1);
                countDown();
            },1000);
        }else if(remainSeconds == 0){//秒杀进行中
            $("#buyButton").attr("disabled", false);
            if(timeout){
                clearTimeout(timeout);
            }
            if(endSeconds>0){
                $("#miaoshaTip").html("秒杀进行中");
            }else{
                $("#remainSeconds").val(remainSeconds - 1);
            }
            endtimeout = setTimeout(function(){
                $("#endSeconds").val(endSeconds - 1);
                countDown();
            },1000);
        }else{//秒杀已经结束
            if(endtimeout){
                clearTimeout(endtimeout);
            }
            $("#buyButton").attr("disabled", true);
            $("#miaoshaTip").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.htm?orderId="+data.data.id;
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function () {
                layer.msg("客户端请求有误")
            }
        });
    }

</script>

订单详情页静态化

看上边js代码的 doMiaosha方法,我们需要修改url:"/miaosha/do_miaosha"的方法

1.原本MiaoshaController.miaosha方法

@RequestMapping(path = "/do_miaosha",method = RequestMethod.POST)
    public String miaosha(Model model, MiaoshaUser user,
					   @RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return "/login";
    	}
    	//判断库存
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
    	int stock = goods.getStockCount();
    	if(stock <= 0) {
    		model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
    		return "miaosha_fail";
    	}
    	//判断是否已经秒杀到了,不能重复秒杀
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
    		model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
    		return "miaosha_fail";
    	}
    	//减库存 下订单 写入秒杀订单
    	OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
    	model.addAttribute("orderInfo", orderInfo);
    	model.addAttribute("goods", goods);
        return "order_detail";
    }

2.修改后的MiaoshaController.miaosha方法

也是返回json数据

@RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<OrderInfo> miaosha(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	//判断库存
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
    	int stock = goods.getStockCount();
    	if(stock <= 0) {
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//判断是否已经秒杀到了
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
    		return Result.error(CodeMsg.REPEATE_MIAOSHA);
    	}
    	//减库存 下订单 写入秒杀订单
    	OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
        return Result.success(orderInfo);
    }

3.然后根据上边提到的js中的方法判断是否跳转到订单详情页
秒杀项目学习第五章4.同样的方法修改order_detail.html页面为order_detail.htm并放在static目录下
秒杀项目学习第五章
5.修改order_detail.htm的内容
取出Thymeleaf模板,使用ajax请求填充的数据。这里只帖出来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==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>

6.新增OrderController处理请求订单数据

@RequestMapping(path = "/detail",method = RequestMethod.GET)
    @ResponseBody
    public Result<OrderDetailVo> info(Model model, 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);
    }

7.OrderDetailVo 、orderService.getOrderById、orderDao.getOrderById

public class OrderDetailVo {
	private GoodsVo goods;
	private OrderInfo order;

秒杀项目学习第五章

秒杀项目学习第五章

添加Spring对静态文件的配置

#static
spring.resources.add-mappings=true
#对Resolver加缓存
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.html-application-cache=true
#指明静态文件路径
spring.resources.static-locations=classpath:/static/

使用一波
秒杀项目学习第五章

自我总结

  • 页面静态化无非就是使用纯html页面+ajax请求json数据后再填充页面
  • 若A页面跳转到B页面之前需要条件判断可以先在A页面中利用ajax请求判断后再跳转
  • 如果不需要条件判断可以直接跳转到B的静态页面,让B自己用ajax请求数据

题外话GET和POST的区别

  • get是幂等的,无论请求多少次得到的结果都一样
  • post,用于对服务端数据更改

三、超卖问题的解决

库存减为负数

出现问题的原因
秒杀项目学习第五章解决问题:
修改最后的更新sql,数据库更新时会加锁,让数据库保证数据正确性
秒杀项目学习第五章

一个用户秒杀到两件商品

出现问题的原因
开始是一个用户同时发出两个请求,通过controller中方法的如下检验

  • 判断库存
  • 判断是否已经秒杀到了

调用如下方法,造成一个用户秒杀到了两件商品
秒杀项目学习第五章

解决方法:
利用数据库的唯一索引特性,我们可以给miaosha_order表添加一个索引
秒杀项目学习第五章秒杀项目学习第五章
这样在创建订单的时候就会发现没法插入第二个订单造成回滚,防止一个用户同时秒杀两个。但是其实我们一般不会让一个用户同时发出两个请求,比如秒杀前先填个验证码之类的可以请求错开时间。

对于超卖问题一般都是想法从数据库层面解决问题

为了压测提高性能把OrderService.getMiaoshaOrderByUserIdGoodsId修改为对象缓存

1.以前的getMiaoshaOrderByUserIdGoodsId方法
秒杀项目学习第五章
2.修改一下直接从缓存中取
秒杀项目学习第五章
OrderKey

秒杀项目学习第五章3.为了能直接从缓存里取生成订单的时候得写入缓存
OrderService.createOrder
秒杀项目学习第五章

四、静态资源优化

  1. JS/CSS压缩,减少流量,如xx.min.js
  2. 多个JS/CSS组合,减少连接数,可以参考tengine
  3. CDN就近访问,多个节点缓存数据,找到离客户端最近的节点

总结

  1. 从浏览器开始使用页面静态化技术(前后端分离)缓存页面
  2. 浏览器发出请求时经过CDN处理
  3. 请求到达服务器时可以使用页面缓存、URL缓存、对象缓存
  4. 总之想尽办法减少对数据库的访问,但是也要考虑数据不一致的问题
相关标签: 秒杀项目