目录
第一章 项目框架搭建
第二章 实现登录功能
数据库设计
明文密码两次MD5处理
JSR303参数校验+全局异常处理器
分布式session
第三章 实现秒杀功能
数据库设计
商品列表页
商品详情页
秒杀功能实现
订单详情页
第四章 JMeter压测
JMeter入门
自定义变量模拟多用户
JMeter命令行使用
Redis的压测工具redis-benchmark
Spring Boot打war包
第五章 页面优化技术
页面缓存+URL缓存+对象缓存
页面静态化,前后端分离
静态资源优化
CDN优化
解决超卖问题
第六章 接口优化
Redis预减库存减少数据库的访问
内存标记减少Redis访问
系统初始化时,把商品的库存加载到Redis
请求先入队缓冲,异步下单,增强用户体验
Nginx水平扩展
压测
第七章 安全优化
秒杀接口地址隐藏
数学公式验证码
接口的限流防刷
第八章 总结
导入启动项,在application.properties写入相关的配置。可以使用声明式事务控制
安装Redis
启动Redis:在cmd下切到redis目录:cd C:\Program Files\Redis-x64-3.0.504然后使用命令:redis-server redis.windows.conf
在另一个cmd下使用redis-cli来使用redis
修改配置文件
哪台机器能访问这个redis,因为做分布式所以要设置所有机器能访问
设置密码
可以将redis加入系统的服务(加不进去不知道为啥,每次用手动启动)
Redis和springboot集成:
依赖:
添加Jedis依赖
添加Fastjson依赖:序列化时将java对象转化成json字符串写到redis服务器中(序列化后可读,方便查看)
配置:
通过ConfigurationProperties注解可以将配置文件有关redis的配置项读取进来并赋值给相应属性
将代表Redis配置信息的RedisConfig的类通过@Component添加到容器中
通过JedisPool获取Jedis对象,通过Jedis对Redis进行操作,在插入数据的时候,将bean转换为string插入,获取数据的时候将str转换为对应的bean(使用fastjson)
出现的问题:多个人向redis中插入的数据key相同会将数据覆盖?
解决思路:在进行插入数据的时候加入前缀,比如用户数据表,前缀就为USER(前缀可通过插入数据类型的类名获得)
模板模型:接口定义需要实现的功能,抽象类定义通用方法,实现类实现特有的方法.
通过KeyPrefix类来生成带有前缀的key,最终的key为:类名+指定的prefix+原来的key
用户端passMD5加密后传入服务端,服务端得到用户端传来的pass再进行MD5存入数据库。
第一次MD5防止用户密码在网络上传输不安全,第二次MD5若数据库被盗防止被反查找出密码。
静态文件
bootstrap画页面
jquery-validation错误表单验证
layer做弹框
md5
jquery
common应用自己的
登录功能实现:使用正则表达式判断mobile是否格式正确.用户输入密码时,先进行MD5
加固定盐值的加密再进行传输,传到服务端后,服务端根据mobile获取用户信息,包括password和salt,取这个salt对这个密码再进行一次MD5加密,最终和数据库的password比较,若相同登录成功,若不同,登陆失败.
JSR 303 用于对Java Bean 中的字段的值进行验证,使得验证逻辑从业务代码中脱离出来
登录时参数校验
现在需要校验的参数上加@Valid标签
然后在需要校验对象的属性中:
可以使用自带的注解进行参数校验,也可以自己写参数校验规则,如@IsMobile
全部异常处理:
拦截异常以更友好的形式呈现而不是exception的堆栈,捕捉到异常放回Result包装codemsg。在进行登陆的时候,若验证不成功,直接抛异常,将异常交给全局异常来进行处理,代码更加简洁
/**
* 在Spring 3.2中,新增了@ControllerAdvice、@RestControllerAdvice 注解,可以用于定义@ExceptionHandler、
* @InitBinder、@ModelAttribute,并应用到所有@RequestMapping、@PostMapping, @GetMapping注解中。
* 可以将异常拦截进行处理
*/
@ControllerAdvice
@ResponseBody
public class GlobleExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(HttpServletRequest request,Exception e){
if(e instanceof BindException){
BindException ex = (BindException) e;
List error = ex.getAllErrors();
ObjectError error1 = error.get(0);
String msg = error1.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs());
}else{
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
//Token是一个字符串,是一段根据特殊规则生成的唯一的、保存在缓存服务器(如Redis)中的key,这个key对应的value是用户账户
// 数据。每个用户登录后,服务器生成一个类似Token并缓存,之后用户每次请求中都要带上这个Token,以便实现类似
// 于HTTP Session的会话跟踪。
把session信息存放到第三方缓存中
在第一次登录的时候,生成一个随机的token,将token:user存入redis中,同时将token存入cookie中返回给客户端,客户端下次请求的时候携带上这个token就可以获取用户信息。将seesion存入缓存中。
问题:每次生成新的token存入redis,怎么把旧的删除
WebMvcConfigurerAdapter
分布式session相关
设置单独的秒杀商品表和秒杀订单表而不是在商品或订单后面加一个标识标识其为秒杀商品:活动很多难以维护
问题:使用mybatis generator逆向工程创建pojo和dao及相应的xml
查询商品秒杀商品列表
sql的左连接
@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date from miaosha_goods mg left join goods g on mg.goods_id=g.id")
public List ListGoodsVo();
改进点:价格用decimal来表示,浮点数精度丢失
封装GoodsVO过去goods信息和miaoshagoods信息,然后给model传递到前端,同时秒杀状态和秒杀倒计时也通过model传递到前端。
秒杀倒计时功能
config包下面的代码
思考不通过thmpeleaf测试功能,转发
查是否登录,未登录返回登录页面,查是否已经秒杀过,一个用户只能秒杀一次,已经秒杀过返回异常页面
减库存,生成order_info 生成miaoshao_order(必须是事务)
下载解压直接点击bin目录下的jmeter.bat即可启动(图形界面)
每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
添加线程组
添加HTTP请求的默认值
添加sample-HTTP
查看结果:监听器-聚合报告
没有优化之前,对于goods/to_list接口,10000的请求量,qps为304.2
查了一次数据库,瓶颈应该在数据库上
@RequestMapping("/to_list")
public String list(Model model, MiaoshaUser user) {
// if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
// return "login";
// }
// String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
// MiaoshaUser miaoshaUser = userService.getByToken(response,token);
model.addAttribute("user", user);
List goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList",goodsList);
return "goodslist";
}
资源监听器:perfmon.msc 瓶颈在数据库
压测:user/info接口,通过token获取用户信息接口。带参数的请求,参数为token,10000的请求量,qps为614.3
后面的方法QPS高,因为获取用户信息的时候读了Redis缓存
上述是在参数为同一个进行压测
需要参数为不同时:
需要传出代表参数的文件
引用:${UserToken}
Redis本身自带的压测工具redis-benchmark。
压测需要一段时间,因为它需要依次压测多个命令的结果,如:get、set、incr、lpush等等
-c 100个并发连接处理100000条请求,需要50.92秒(每秒1963.83条命令)(以三字节传输)
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
存储大小为100字节的数据包(以100字节传输)-q只做简单的输出
redis-benchmark -h 127.0.0.1 -p 6379 -q -d 100
只测试某些操作的性能,-t只测试指定的操作
redis-benchmark -t set,get -n 100000 -q
只测试某些数值存取的性能
redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
war包和jar包的区别
压测to_list接口,50000条请求,后面以此基准做优化,QPS为325.2/sec
主要的接口是miaosha/do_miaosha,这个接口做压测
先生成5000条MiaoshaUser数据插入数据库,然后登录获得每一个用户的token存入redis
生成token文件(一共5000条记录)
然后配置Jmeter
多线程会出现超卖问题,库存变为负数
页面缓存:先从缓存中找,找到就直接返回,找不到再进行渲染,然后缓存到客户端。(页面缓存一般有效期较短)(为了防止一个时间段大量访问才使用)
/**
* 直接返回html
*/
@RequestMapping(value = "/to_list",produces = "text/html")
@ResponseBody
public String list(HttpServletRequest request,HttpServletResponse response,Model model, MiaoshaUser user) {
// if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
// return "login";
// }
// String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
// MiaoshaUser miaoshaUser = userService.getByToken(response,token);
model.addAttribute("user", user);
List goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList",goodsList);
String html = redisService.get(GoodsKey.getGoodsList,"",String.class);
if(!StringUtils.isEmpty(html)){
return html;
}
//手动渲染,存入缓存
IWebContext ctx = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodslist",ctx);
if(!StringUtils.isEmpty(html)){
//保存到缓存中
redisService.set(GoodsKey.getGoodsList,"",html);
}
return html;
}
URL缓存:商品详情页面
对象缓存(更细粒度的缓存):通过id获取到用户之后存入缓存,下次可以直接从缓存中取,通过token获取用户也是对象缓存
public MiaoshaUser getById(long id){
//取缓存
MiaoshaUser miaoshaUser = redisService.get(MiaoshaUserKey.getById,""+id,MiaoshaUser.class);
if(miaoshaUser!=null){
return miaoshaUser;
}
//取数据库
miaoshaUser = miaoshaUserDao.getById(id);
if(miaoshaUser!=null){
//存入缓存
redisService.set(MiaoshaUserKey.getById,""+id,MiaoshaUser.class);
}
return miaoshaUser;
}
将页面缓存到用户的浏览器上
@Update("update miaosha_goods set stock_count=stock_count-1 where goods_id=#{goodsId} and stock_count>0")
public int reduceStock(MiaoshaGoods miaoshaGoods);
在更新库存的时候,判断库存是否大于0,大于0才进行更新
解决一个用户同时可以秒杀两个商品:在创建秒杀订单的时候会创建另个,一个是秒杀订单,一个是订单详情.将秒杀订单的userid设置为唯一索引,那么第二条记录就不能插入.
@RequestMapping("/do_miaosha")
@ResponseBody
public Result miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
model.addAttribute("user",user);
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//判断库存
GoodsVo goods = goodsDao.getGoodsVoByGoodsId(goodsId);
int stock = goods.getGoodsStock();
if(stock<=0){
model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
//判断是否已经秒杀到(一个人只能秒杀一件商品)
MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
if(miaoshaOrder!=null){
model.addAttribute("errmsg",CodeMsg.REPEATE_MIAOSHA.getMsg());
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
//减库存 下订单 写入秒杀订单(必须是事务)
OrderInfo orderInfo = miaoshaService.miaosha(user,goods);
model.addAttribute("orderInfo",orderInfo);
model.addAttribute("goods",goods);
return Result.success(orderInfo);
}
优化1:判断是否秒杀到,查是否有已经秒少到商品的时候,查缓存,若有直接返回错误,没有就查数据库,然后再写入缓存中.
思路:减少数据库的访问
RabbitMq安装
https://www.cnblogs.com/vaiyanzi/p/9531607.html
解决中文路径问题
https://www.cnblogs.com/bade/p/10303687.html
SpringBoot集成RabbitMQ
配置文件
#rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#\u6D88\u8D39\u8005\u6570\u91CF
#消费者数量
spring.rabbitmq.listener.simple.concurrency= 10
#最大消费者数量
spring.rabbitmq.listener.simple.max-concurrency= 10
#\u6D88\u8D39\u8005\u6BCF\u6B21\u4ECE\u961F\u5217\u83B7\u53D6\u7684\u6D88\u606F\u6570\u91CF
#消费者每次从队列获取的消息数量。写多了,如果长时间得不到消费,数据就一直得不到处理
spring.rabbitmq.listener.simple.prefetch= 1
#\u6D88\u8D39\u8005\u81EA\u52A8\u542F\u52A8
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#\u6D88\u8D39\u5931\u8D25\uFF0C\u81EA\u52A8\u91CD\u65B0\u5165\u961F
#消费者消费失败,自动重新入队
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#\u542F\u7528\u53D1\u9001\u91CD\u8BD5
#启用发送重试 队列满了发不进去时启动重试
spring.rabbitmq.template.retry.enabled=true
#1秒钟后重试一次
spring.rabbitmq.template.retry.initial-interval=1000
#最大重试次数 3次
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10秒钟
spring.rabbitmq.template.retry.max-interval=10000
#等待间隔 的倍数。如果为2 第一次 乘以2 等1秒, 第二次 乘以2 等2秒 ,第三次 乘以2 等4秒
spring.rabbitmq.template.retry.multiplier=1.0
rabbitmq的四种交换机模式
实现InitializingBean接口,重写afterPropertiesSet()方法,这个方法将在系统初始化的时候调用.
@Override
public void afterPropertiesSet() throws Exception {
List goodsVoList = goodsService.listGoodsVo();
if(goodsVoList==null){
return;
}
for(GoodsVo goods:goodsVoList){
redisService.set(GoodsKey.getMiaoshaGoodsStock,""+goods.getId(),goods.getStockCount());
}
}
使用一个map存储商品是否秒杀完毕,map里面存存goodid:false/true,标记该商品是否秒杀完毕。访问redis之前先判断map中的值,防止一直访问redis。当redis中的库存小于0时,将该商品标记为秒杀完毕。
思路:秒杀开始之前,先去请求接口获取秒杀地址
获取秒杀的path,将path存入redis之中,秒杀的时候验证path
思路:点击秒杀之前,先输入验证码,分散用户请求
对接口做限流