学习秒杀系统-页面优化技术

文章目录

前言

本篇重点学习页面缓存,页面静态化,剩下的了解即可。

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

业务的瓶颈就是数据库,而这些页面(大粒度),对象(小粒度)都需要查询数据库 所以最大力度加缓存。

页面缓存

什么是页面缓存?我们访问一个页面的时候,不是让服务端渲染而是从缓存里边去,如果找到返回给客户端,如果没有再让服务端渲染并返回给客户端,同时保存到redis中。

与之前没有页面缓存相比,省时省力在哪?

首先,之前的方法是我们每次请求一个页面,服务端都需要从数据库中查询相关数据并渲染到页面中然后返回给客户端
页面缓存如何改进的? 直接从缓存中获取页面的信息并直接返回给客户端。 省去查询数据库和渲染页面。什么是渲染页面?在服务端渲染页面时从接受请求开始,查询数据库,然后加载JS/CSS文件再将动态数据添加到页面中,最后返回一个完整的html页面的过程

以商品列表为例

如何实现?1取缓存 2缓存未命中的话手动渲染页面并返回

取缓存

bash 复制代码
//如果缓存中有页面且TTL没有过期
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
    	if(!StringUtils.isEmpty(html)) {
    		return html;
    	}

手动渲染

bash 复制代码
		String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
    	if(!StringUtils.isEmpty(html)) {
    		return html;
    	}
    	//如果没有先查询数据库
    	List<GoodsVo> goodsList = goodsService.listGoodsVo();
    	//添加到页面中
    	model.addAttribute("goodsList", goodsList);
    	SpringWebContext ctx = new SpringWebContext(request,response,
    			request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
    	//手动渲染,goods_list是具体html文件,要不然thymeleafViewResolver也不知道页面长啥样啊
    	html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
    	//存储到缓存中
    	if(!StringUtils.isEmpty(html)) {
    		redisService.set(GoodsKey.getGoodsList, "", html);
    	}
 
    	return html;

这里需要注意的是,我们将页面缓存到redis时都会设置有效期,过了有效期我们就需要重新从数据库中加载

URL缓存

URL缓存本质上就是页面缓存的一种,只不过需要在地址栏中传参数。区别就是get和set方法都要有路径的参数

bash 复制代码
    @RequestMapping(value="/to_detail2/{goodsId}",produces="text/html")
    @ResponseBody
    public String detail2(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;
    	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";
    	
    	SpringWebContext ctx = new SpringWebContext(request,response,
    			request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
    	html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
    	if(!StringUtils.isEmpty(html)) {
    		redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
    	}
    	return html;
    }
    

对象缓存

像我们之前缓存user一样,每次从缓存中取出对应token的user。但我们这里的有效期是动态的,也就是用户在有效期没过期之前登录了就会自动延长有效期(具体就是从缓存里边查询出来不为空,就延长)。和之前的静态有效期有区别。

页面静态化,前后端分离(常用)

为什么有这个呢?

你如果只做了页面缓存,会发现渲染页面其实本质上还是服务端做

但你做了页面静态化,浏览器会将html页面缓存在客户端,这样页面数据渲染时客户端做,遇到动态的数据再去请求服务端。

前端:对于动态数据我们需要获取,添加以下方法

bash 复制代码
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("秒杀已经结束");
	}
}

后端:需要取出相关数据并返回,这里注意方法要加@ResponseBody 因为是返回数据,不是返回页面了。

bash 复制代码
    @RequestMapping(value="/detail/{goodsId}")
    @ResponseBody
    public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, 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);
    }

这样是不是感觉比页面缓存还慢呀,因为每次访问时确实是浏览器解析页面,但动态数据都需要去加载动态数据,而加载动态数据就需要查询数据库。而页面缓存直接可以从缓存中取。其实这里还会有个配置,当没过期时,浏览器直接加载缓存中的静态页面。无需请求服务端,当过期时,则需要重新访问服务器重新加载动态数据。若在过期时间内,我们更新了数据,客户端不会自动收到提示,我们需要主动提示客户端我们更新数据了(这部分具体实现,之后会说,大家先了解这些原理)。

bash 复制代码
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/

当我们点击秒杀时前端:

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

需要将商品id传递过去 ,这里为什么是post(重点)

GET POST区别

这道题面试会经常问,网上答案很多,最重要的区别是以下两点(不要再说查询数据和请求数据了,这俩本质上都能查询和请求)

GET 幂等 无论你get多少次,服务端数据不会变化,且状态不会改变

POST 显然不幂等 你提交数据(新增,添加,修改,删除),服务端数据可能会变化,会改变状态。

改变状态怎么理解?比如登录方法 客户端请求方式为POST ,服务端数据不会发生改变,但是登录完了之后会创建session啊,生成token等等一系列操作。

为什么 <a href="/delete?id=xxx">是很严重的错误?

1 首先a标签是超链接 它是get方法,服务端数据不应该发生变化。

2 其次用户很容伪造一些get方法来删除数据,因为get方法主要通过路径参数来删除数据。

改为post就安全了嘛? 是的 post方法通过请求体传参,而请求体里边可以包含token,我们更容易通过token先校验然后再取数据操作。

如何解决超卖?重复卖?(简单版)

解决超卖

本节课解决超卖的方式很简单,就是将更新数据库时的SQL语句加个判断条件,如果库存大于0则执行更新。为什么在这里加可以,因为数据库系统 会采用MVCC机制(主页里讲过),修改操作(增,删,改)sql语句会加锁,所以多线程并发访问时,一个表只会有一个线程修改,所以我们可以直接在SQL语句中加上判断条件。

解决重复卖?

什么是重复卖?一个用户对于秒杀商品购买了两次

如何解决 为相应的字段的添加唯一索引。比如秒杀商品表中有用户id和商品id字段我们为这两个字段加上唯一索引。这样用户id和商品id的字段值在此表中只能有一个,重复秒杀会报错。

唯一索引并不是指"这张表中只有一个索引",而是指"这个索引要求字段值唯一"。一张表可以有多个索引,包括多个普通索引和多个唯一索引。

普通索引:允许表中的字段值重复。例如,在 users 表的 username 字段上创建普通索引,可以快速查找 username,但同一个 username 可以出现多次。

唯一索引:要求表中的字段值必须唯一。如果尝试插入或更新违反唯一性约束的数据,数据库会抛出错误。

静态资源优化

这些在我们代码中体现不出来,所以自行search吧 。面试中也不是特别重要

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

CDN优化

总结

并发大的瓶颈 是在于数据库

如何解决?

加缓存,首先用户发起请求,浏览器通过页面静态化,可以直接把页面缓存到浏览器,请求到达服务端之前请求会首先访问CDN节点。通过CDN,继续到服务端之前 还可以加ngix缓存,ngix没有就是服务端的页面缓存,然后在细粒度是对象缓存,最后才是数据库。

相关推荐
ZC1111K3 小时前
maven(配置)
java·maven
慕y2744 小时前
Java学习第五十八部分——设计模式
java·学习·设计模式
躲在云朵里`5 小时前
SpringBoot的介绍和项目搭建
java·spring boot·后端
菜还不练就废了5 小时前
7.19-7.20 Java基础 | File类 I/O流学习笔记
java·笔记·学习
Yweir5 小时前
Elastic Search 8.x 分片和常见性能优化
java·python·elasticsearch
设计师小聂!5 小时前
尚庭公寓--------登陆流程介绍以及功能代码
java·spring boot·maven·mybatis·idea
为什么名字不能重复呢?6 小时前
Day1||Vue指令学习
前端·vue.js·学习
明早你自己说6 小时前
学习寄存器——GPIO(二)学习BSRR BRR ODR寄存器的原子性和在HAL库的应用
单片机·嵌入式硬件·学习
倔强青铜三6 小时前
苦练Python第27天:嵌套数据结构
人工智能·python·面试