Java高性能高并发实战之页面优化技术(五)

前言

此篇文章是系列的第五章篇文章,具体文章目录:

章节名称 博客地址
安装部署Redis 集成Redis(已完结)
页面登陆功能设计 登录功能设计(更新优化中)
秒杀页面具体设计 秒杀详情页(已完结)
JMeter初级压测学习 Jmeter压测入门学习(已完结)
页面优化设计 页面优化设计(已完结)
接口优化 RabbbitMq接口优化(已完结)
图形验证码等 图形验证码及接口防刷(更新优化中)

在前面学习到了基础的登录功能模块的设计(还在继续完善中)以及Redis封装类的设置对应的是github源码miaosha_2miaosha_2模块。对于模块三部分主要是关于秒杀商品页面的设计主要是权限设计和秒杀时间的倒计时设计后续会持续推出,这部分主要来讲述页面的优化部分的设计。

正文

对于秒杀业务的瓶颈大部分都是来源于数据库的设计,因为对于商品而言商家的增添,用户的购买减少商品的数量,以及用户的查询都是会访问到数据库,能够降低数据库的访问是我们在秒杀业务中要做的重中之重,所以这部分的页面优化我们主要考虑来减少对数据库的访问。

增加缓存

而第一种最有效减少对数据库的访问就是加缓存,通过各种粒度的缓存来实现减少对数据库的访问,通常用 页面缓存+ URL缓存+对象缓存,最大粒度的就是页面缓存,最小粒度的就是对象缓存。将这些缓存的信息缓存在Redis中,后面可以使用到RDM来进行具体查看,发现缓存了整个页面。

页面静态化,前后端分离

什么是页面静态化:对于现在的很多的应用程序都是使用到JSP页面或是thymeleaf 模板解析也都是动态化的。静态化就是单纯的html页面,通过js,ajax来请求服务端,进行页面的渲染等。
好处: 前面提到了使用页面缓存技术,但是客户端还是会请求服务端来进行页面的下载,但是若是做了页面的静态化,客户端就相当于缓存了前端的静态页面,不需要次次的请求都从服务端进行页面数据的下载。

页面缓存

概念简述: 当我们在访问一个页面的时候,并不是就会立马前往服务端请求页面数据的下载,而是先在缓存中查看有没有这个页面数据,若是存在就会返回给客户端,若是没有再去服务端请求数据手动渲染数据,返回给客户端,然后存在缓存中。所以就包含以下的三步:

  • 去缓存
  • 手动渲染模板
  • 结果输出

实际操作

在我们实现页面缓存之前先来看一下之前是如何操作的,可以去github上下载源代码,对于前4部分是没有使用到页面缓存的,所以我们先来打开miaosha_4模块看之前是如何将我们的商品列表给渲染出来的。

  1. 首先是访问固定的商品列表的url:localhost:8080/goods/to_list查看代码:key看到是访问到固定的url以后,会在进行一系列的Controller-> Service-> dao从数据库中取出数据,然后存在一个list中,放在model中返回给商品列表页,这个时候对于用户的每一次访问到前端页面都会进行数据库的请求,这个时候在高并发的情况下肯定是不实用的,所以我们要进行思考和更改。
    如下是之前的操作:
    @RequestMapping(value="/to_list")
    public String list(Model model,MiaoshaUser user) {
    	model.addAttribute("user", user);
    	List<GoodsVo> goodsList = goodsService.listGoodsVo();
    	model.addAttribute("goodsList", goodsList);
    	 return "goods_list";
    }

这个时候就需要用到了thymeleafViewResolver 具体可以参看参看地址使用ctrl+f进行全局搜索。既然知道使用到这个,就需要我们使用*@Autowired*注入:

    @Autowired
	ThymeleafViewResolver thymeleafViewResolver;

然后使用到下面这个方法(这个方法就是用来做缓存功能):

thymeleafViewResolver.getTemplateEngine().process();

但是我们需要传参数,就可以使用到快捷键 ctrl+P查看需要什么具体的参数:
在这里插入图片描述
这里我们可能会疑惑,IContext类型又是个什么类型呢,这个时候使用到双击shift进行全局的搜索:
可以看到的是如下,然后我们点击进行:
在这里插入图片描述
点击进去之后使用快捷键ctrl+ alt+B查看相关的实现:查看与Spring相关联的配置(因为我们是Spring项目进行整合)。
在这里插入图片描述
可以发现若是我们想要使用SpringWebContext就需要传入参数呀,我们找到对应的实现,见下图:我们传入这些参数,然后new一个IContext类型的对象传入到我们的函数中,才能够实现其功能。

在这里插入图片描述
代码如下:

   @RequestMapping(value="/to_list", produces="text/html")
    @ResponseBody
    public String list(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user) {
    	model.addAttribute("user", user);
    	//取缓存
    	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);
		// new了一个ctx 传入上面要求的参数,然后传入到process函数中即可返回我们的页面信息。
		html=thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx);
		if(!StringUtils.isEmpty(html)){
			redisService.set(GoodsKey.getGoodsList,"",html);
			// 判断缓存是否是空。
		}
    	return html;
    }

注意在之前的文章中我们已经封装了对应的redisService 所以这里将GoodsKey对应的缓存也实现相对应的加入:

public class GoodsKey extends BasePrefix{

	private GoodsKey(int expireSeconds, String prefix) {
		super(expireSeconds, prefix);
	}
	public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
	// 这里设置一个过期时间,我们思考一下对于页面缓存的处理是为了防止在瞬间大量的用户请求到同一个页面而导致服务端响应超载,这里设置过期时间为60s,
	//表示这个页面缓存为一分钟,在一分钟里面再度访问就可以请求到这个缓存,但是却不能够一直留有这个缓存的信息,因为页面会发生变动。
	//我们可以接受自己的页面是一分钟之前的页面,再刷新就好了,但是若是保存的一直都是之前的页面那体验也未免太差了。
	public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
}

以上都完成以后就可以返回页面,我们做一个测试,访问http://localhost:8080/goods/to_list页面,然后页面信息就会缓存到redis中,我们在reids客户端中进行查看,发现将html页面都给缓存了下来如下图所示,这个时候当在高并发的情况下很多人都有访问到这个页面时候,就会从Redis中取出这个数据而不是再度进行数据库的查询,也就减少了对数据库的访问。
可以看到对应的key值的格式(具体可参见redis数据封装那一节的设计,一方面是使用到缓存,另外一方面是防止Key值的重复)
在这里插入图片描述

URL缓存

对于URL缓存的大致思想其实和前面的商品列表是类似的也都是使用到thymeleafViewResolver 但是也有不同的地方。
举个栗子:对于商品的列表页面,对于任何人的访问列表页面都是一样的,但是对于商品的详情页面,我们每个商品的显示的列表页面应该不同,所以再存入到redis缓存中时候对于页面缓存,我们这样写:

	redisService.set(GoodsKey.getGoodsList,"",html);
	// 可以看到的是对于 中间的参数表示key值为空,表示无差别key值。
	```
	对于URL缓存我们可以以details为例:
	```java
		redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
		// key值传入时候需要带上商品的id,这样就可以根据具体的id信息来进行缓存处理操作。

对象缓存

在前面完成了对于页面缓存:缓存哪些在短时间内访问很高的页面加入到redis缓存中,可以让用户在访问同一个页面的时候不会每次都会请求到数据库,但是这个页面缓存也是有时间限制的我们限制在一分钟,就是说你过了一分钟之后再度请求这个页面,若你还是第一次请求,那么就会再度请求到数据库,然后再度加入缓存。
对于URL缓存的粒度会更加细,对于商品的列表页面是无差别的,不同的用户也会显示相同的页面,但是对于商品的详情页面就需要根据具体的商品的id信息来具体显示所以URL缓存会更加对应匹配。
对于对象缓存也是属于缓存类的一种,顾名思义就是缓存当前用户对象的具体数据,这样对于每一个用户就会有不同的页面以及数据显示。

具体实现思想

在之前我们没有设置对象缓存之前会根据传入的id信息来查询数据库,来判断当前用户的登录的账号(手机号码是否存在)

// 调用底层的查询数据库。
public MiaoshaUser getById(long id) {
		return miaoshaUserDao.getById(id);
	}

这里就可以使用到对象缓存,之前在登录功能设计中有设计到使用一个唯一的token信息来实现分布式session功能(具体原因上一篇文章中有介绍),所以这里就思考能不能实现对token信息的缓存处理,让在查询数据库之前先进行缓存的查询。然后再进行一系列的操作。

具体实现过程

  1. 既然想要将token信息添加到redis缓存中,就需要先生成对应的getById函数
public static  MiaoshaUserKey getById=new MiaoshaUserKey(0,"id");
// 表示无过期时间
  1. 完成以后就可以在redis中进行查询:
//取缓存
		MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
  1. 如果不为空表示查询到了对应的信息,正确返回,若是为空表示之前的信息没有就需要将信息加入到缓存中
redisService.set(MiaoshaUserKey.getById, ""+id, user);

所以以上代码为:

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;
	}

可能这里还看不出来对于页面缓存,URL缓存和对象缓存的具体区别,区别在于对象缓存的用户信息的修改。前面我们设置了页面和URL,这个时候我们同时设置了过期时间:
在这里插入图片描述
表示对于这些缓存的信息只能够保存一分钟(具体实现过程就不细讲了),但是我们的对象缓存是永久的这个时候若是对密码进行更改该如何操作呢?
4. 首先根据之前的修改过的查询查询出来用户的信息。
5. 然后对具体的密码进行定点更改。
6. 完成之后将原来保存的key值进行删除,因为用户的密码已经更改成功。
7. 将之前保存的用户的token信息进行更新(注意不是删除,删除了token以后就无法正常登录)。

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()));
		miaoshaUserDao.update(toBeUpdate);
		//处理缓存
		redisService.delete(MiaoshaUserKey.getById, ""+id);
		user.setPassword(toBeUpdate.getPassword());
		redisService.set(MiaoshaUserKey.token, token, user);
		return true;
	}

页面静态化

前面讲述的都是一些关于缓存的信息下面部分来具体讲述关于静态化的处理。
更为激进的一种缓存的方式,就是直接把一些页面数据下载到本地的浏览器上,在访问时候直接访问本地数据,而不需要去和服务器进行交互。对于这部分的操作我们就以goods_detail页面为例,对于之前的页面布局是这样的:

<html xmlns:th="http://www.thymeleaf.org">
<!-- 可以看到的是引用了模板信息有引用到thymeleaf 模板解析等,
并且下方的引用都是th:src="@{};类型表示使用到的是thymeleaf的解析,我们要实现页面的静态化就需要将这些模板的解析都给拿掉-->
<head>
    <title>商品详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
    <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- md5.js -->
    <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>

对于页面静态化来说,这些模板解析都需要去掉,使其访问路径为直接的绝对路径。如下代码所示,这样一来就可以将相对的信息读取到本地浏览器缓存中,而不是每次的请求都需要再访问到服务器端进行页面的渲染。

<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>

当然以上只是界面的引用信息,对于基础的html代码我们也来看一看一些细微的变化,就举个立即秒杀的栗子:对于版本5之前都是这样的提交方式,这样在我们点击立即秒杀时候需要提交表单到服务端,这个时候我们就需要进行一些变化,让button按钮绑定到对应的函数实现ajax请求,只请求部分的信息。

<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
   <!-- 定义一个form表达,提交到如上的地址-->
        		<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
        		<input type="hidden" name="goodsId" th:value="${goods.id}" />
        	</form>

对于静态化之后的立即秒杀就是一个简单的button按钮具体如下:

   <!-- 使用ajax方式做请求处理 -->
<button class="btn btn-primary btn-block" type="button" id="buyButton"οnclick="doMiaosha()">立即秒杀</button>
  <input type="hidden" name="goodsId"  id="goodsId" />

下面我们来看一下对应的doMiaosha()函数:

// 使用ajax方式来进行局部信息的提交。
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("客户端请求有误");
		}
	});
}

将一些页面需要的主要信息都不再请求数据库,而是使用参数对象的方式将具体的信息映射到前端的页面上,实现页面的静态化。不只是对于商品详情页,对于秒杀商品页或是订单详情页都有对应的静态化操作,大家将源码下载下来以后可以进行具体的查看,调试等。这里就不再进行一一的详细解释。
在这里插入图片描述

后记

(对于sql部分参见秒杀页面设计中sql语句)
对于页面优化技术这里主要讲诉了三种不同粒度的缓存和页面静态化的处理,涉及的业务代码这里没有进行具体的讲解,讲解的也都是思想,想要效仿学习大家可以自行下载代码进行调试处置。源码部分不是很难,有些地方也都有具体的注释。个人的理解也是有限,所以有些地方没有讲到或是讲错的地方还希望大家能够指证出来,大家一起学习,后续还会持续对这篇文章进行一个更正与添加,也算是激励自己持续学习。
源码参见Github中有全部的部分,对于这部分对应的是miaosha_5可以与最近的miaosha_4进行一个对比与记录。

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/105899679