商城秒杀系统的实现(四)页面级高并发秒杀优化

优化思路

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

(一)页面缓存+ URL缓存+对象缓存

redis缓存详情页

(1)页面缓存:商品列表

  1. 取缓存
  2. 手动渲染模版
  3. 结果输出

访问一个页面的时候不是直接系统渲染,而是从缓存里面取,缓存里面没有才去渲染(同时缓存到redis当中)

以goods_list为例:

因为要把goodsList存到redis当中,所以在redis包中new一个class:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29TFn51l-1581767254371)(/Users/dylan/Library/Application Support/typora-user-images/image-20200211213815473.png)]

/* GoodsController */

//原本是这样的
@RequestMapping("to_list")
	public String list(Model model,MiaoshaUser user){
		model.addAttribute("user",user);
		List<GoodsVo> listGoodsVo = goodsService.listGoodsVo();
		model.addAttribute("goodsList",listGoodsVo);
		return "goods_list";
}

//变成这样
@Autowired
ThymeleadViewResolver thymeleadViewResolver;

@Autowired
ApplicationContext applicationContext;

@RequestMapping(value="to_list",produces="text/html")
@ResponseBody
	public String list(Model model,MiaoshaUser user){
		model.addAttribute("user",user);
    //从缓存里面先取出来
	    String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
	    //如果不为空就直接返回
			if(!StringUtils.isEmpty(html)){
					return html;
	    }
		List<GoodsVo> listGoodsVo = goodsService.listGoodsVo();
		model.addAttribute("goodsList",listGoodsVo);
		//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;
}

手动渲染的时候,我们需要利用

thymeleafViewResolver.getTemplateEngine().process(String templates, webContext ctx)

这个方法

首先要生成WebContext

WebContext ctx = new WebContext( request, response, request.getServletContext(), request.getLocale(),model.asMap()); 

再传入到上面那个方法里面就可以了

注意点

  1. 页面有效期比较短,所以需要修改KeyPrefix中的expireSeconds

    package com.pro.miaosha.redis;
    
    public class GoodsKey extends BasePrefix{
    	private GoodsKey(int expireSeconds,String prefix) {
    		super(expireSeconds,prefix);
    	}
    	public static GoodsKey getGoodsList = new GoodsKey(60,"gl");//这里设置时间为60s
    }
    
  2. 验证:访问完http://localhost:8080/goods/to_list之后查看redis发现已经被缓存了

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ETZzAVH-1581767254372)(/Users/dylan/Library/Application Support/typora-user-images/image-20200211223427098.png)]

(2) URL缓存:商品详情

商品详情页面:

//原本是这样的
@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;
		
		if(now < startAt) {
			miaoshaStatus = 0;
			remainSeconds = (int)((startAt-now)/1000);
		}
		else if(now > endAt) {
			miaoshaStatus = 2;
			remainSeconds = -1;
		}
		else {
			miaoshaStatus = 1;
			remainSeconds = 0;
		}
		model.addAttribute("miaoshaStatus",miaoshaStatus);
		model.addAttribute("remainSeconds",remainSeconds);
		return "goods_detail";
	}

//改成这样
    @RequestMapping(value="/detail/{goodsId}")
    @ResponseBody
    public String detail(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,
    		@PathVariable("goodsId")long goodsId) {
      //从缓存里面先取出来
	    String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);
	    //如果不为空就直接返回
			if(!StringUtils.isEmpty(html)){
					return html;
	    }
    	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;
    	}

	    //否则需要手动渲染
		WebContext ctx = new WebContext( request, response, request.getServletContext(), request.getLocale(),model.asMap()); 
		html = thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx);
	    if(StringUtils.isEmpty(html)){
	      redisService.set(GoodsKey.getGoodsDetail,"",html);
	    }
	    return html;
    }

(关于里面的这个SpringwebContext:)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lRaQM0wF-1581767254373)(/Users/dylan/Library/Application Support/typora-user-images/image-20200211214844057.png)]

String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if(html == null || html.length() <= 0) {
	List<GoodsVo> goodsList = goodsService.listGoodsVo();
	model.addAttribute("goodsList", goodsList);
	//手动渲染模板
	html = renderTemplate("goods_list", model, thymeleafViewResolver,request,response,applicationContext);
	if(html!=null && html.length() > 0) {
		redisService.set(GoodsKey.getGoodsList, "", html);
	}
}

(3)对象缓存:用户token,getById改造

url和页面缓存时间比较短,适合变化不大的页面

以miaoshaUserService为例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWzxpiU4-1581767254375)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212091215249.png)]

改造一下getByid:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vybVeQ4E-1581767254376)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212091250515.png)]

首先在miaoshaUserKey中加上一个新的getById

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ed3imbW-1581767254377)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212091557598.png)]

public MiaoshaUser getByid(long id ){
		MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, MiaoshaUser.class);
		if (user != null) {
			return user;
		}
		user = miaoshaUserDao.getById(id);
		if (user != null) {
			redisService.set(MiaoshaUserKey.getById, "" + id, user);
		}
		return user;
}

但是注意有update这个操作

updatePassword(String token,long id,String newPassword){
	getById(id);
  if(user == null){
		throw new global exception();
  }
  
  //更新数据库成功了
  userToBeUpdate = new miaoshaUser();
  userToBeUpdate.set(id);
  userToBeUpdatt.setPa(md5Util.formpass2dbpass,user.getSalt);
  //修改缓存
  redisService.delete(miaoshaUserKey.getById,""+id);
  user.setPassword(userToBeUpdate)
  redisService.set(miaoshaUserKey.token,token,user);//不删除,
  
}

还需要在redisService中添加一个delete方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfCM7FIT-1581767254378)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212092233710.png)]

public boolean delete(KeyPrefix prefix, String key) {
		Jedis jedis = null;
		try{
			jedis = jedisPool.getResource();
			String realKey = prefix.getPrefix()+key;
			long ret = jedis.del(key);
			return ret > 0;
		}
		finally{
			returnToPool(jedis);
		}
	}

miaoshaUserDao中增加update方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJEk7KYI-1581767254379)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212093500922.png)]

@Update("update miaosha_user set password = #{password} where id = #{id}")
public void update(MiaoshaUser toBeUpdate);

ps:Service 只能调用service不许调用别人的dao

压测商品列表,对比QPS

(二)页面静态化,把页面缓存到客户端

(1)详情页静态化改造

把页面缓存到浏览器上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFopBPFf-1581767254394)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212100215784.png)]

静态化详情页:

//原来是这样的
@ResponseBody
	@RequestMapping(value="/to_detail/{goodsId}",produces = "text/html")
	public String detail(Model model, MiaoshaUser user, @PathVariable("goodsId") long goodsId,HttpServletRequest request,HttpServletResponse response) {

		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;

		if (now < startAt) {
			miaoshaStatus = 0;
			remainSeconds = (int) ((startAt - now) / 1000);
		} else if (now > endAt) {
			miaoshaStatus = 2;
			remainSeconds = -1;
		} else {
			miaoshaStatus = 1;
			remainSeconds = 0;
		}
		model.addAttribute("miaoshaStatus", miaoshaStatus);
		model.addAttribute("remainSeconds", remainSeconds);
		//return "goods_detail";
		// 否则需要手动渲染
		WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(),
				model.asMap());
		html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
		if (!StringUtils.isEmpty(html)) {
			redisService.set(GoodsKey.getGoodsDetail, "", html);
		}
		return html;
	}
//现在
@ResponseBody
	@RequestMapping(value="/to_detail/{goodsId}")
//String -> Result<GoodDetailVo>
	public Result<GoodDetailVo> detail(Model model, MiaoshaUser user, @PathVariable("goodsId") long goodsId,HttpServletRequest request,HttpServletResponse response,MiaoshaUser 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;

		if (now < startAt) {
			miaoshaStatus = 0;
			remainSeconds = (int) ((startAt - now) / 1000);
		} else if (now > endAt) {
			miaoshaStatus = 2;
			remainSeconds = -1;
		} else {
			miaoshaStatus = 1;
			remainSeconds = 0;
		}
		//增加这一部分
    GoodDetailVo vo = new GoodDetailVo();
    vo.setGoods(goods);
    vo.setUser(user);
    
    vo.setRemainSeconds(remainSeconds);
    vo.setMiaoshaStatus(miaoshaStatus);
	
		return Result.success(vo);
	}

添加

public class GoodsDetailVo {
	private int miaoshaStatus = 0;
	private int remainSeconds = 0;
  private MiaoshaUser user;
  private GoodsVo goods;
  //还有getter,setter
}

原本从商品列表跳转到缓存的时候:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2p7l3AfO-1581767254401)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212104705090.png)]

现在要把静态页面放在static里面(顺便把后缀改成htm,如果不改还是会找之前templates里面的那个,虽然我个人觉得这个做法好像不太规范):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2p9TGlt-1581767254408)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212104821244.png)]

再改成这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oTdv5vhH-1581767254410)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212104941897.png)]

goods_detail里面的thymeleaf删除掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvwGXLlf-1581767254412)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212105018889.png)]

这里面th打头的全部删除掉:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RKCh8dz3-1581767254414)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212105123596.png)]

还有@{也删掉:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oe24XgT4-1581767254415)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212105155103.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7az7CoX-1581767254417)(/Users/dylan/Library/Application Support/typora-user-images/image-20200212105458345.png)]

开始写下面的script

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("客户端请求有误");
		}
	});
	
}

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("秒杀已经结束");
	}
}

(2)改造秒杀接口

修改配置:application.properties

spring.resources.add-mappings=true
spring.resources.cache-period= 3600
spring.resources.chain.cache=true 
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/

后端的改动:

/*-----------注释掉的是以前的方法----------------*/
		@RequestMapping(value="/do_miaosha",method = RequestMethod.POST)
    public Result<OrderInfo> list(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
        //return "login";
    		return Result.error(CodeMsg.Session_error);
    	}
    	//判断库存
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
    	int stock = goods.getStockCount();
    	if(stock <= 0) {
        //model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
    		//return "miaosha_fail";
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//判断是否已经秒杀到了
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
        return Result.error(CodeMsg.repeat);
				//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";
			return Result.success(orderInfo);
    }

(3)修改减少库存的SQL

(三)静态资源优化

(1)压缩
(2)合并

(四)CDN就近访问

发布了84 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Mercuriooo/article/details/104333300