引言:
基于SpringBoot框架构建热点商品秒杀项目,亮点: Redis实现分布式Session, 页面缓存,RabbitMQ+接口优化, 线上部署
概述
第一章小点
Redis缓存key值统一设置规则,以及通用缓存Key封装方式。
第二章小点
两次MD5
登陆页面
1 | <!-- jquery --> |
@{/js/jquery.min.js}
中第一个/
代表的是resources/static
路径
required="true" minlength="6" maxlength="16"
是jquery-validator
提供的验证
参数验证
前台传的参数与对象里的属性名字相同spring就会自动包装,比如前台传的参数名字是mobile
和password
,LoginVo
对象里的属性名字也是mobile
和password
就会自动包装
ajax前端传入
1 | <script> |
jsr303参数校验
1 | ({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) |
1 | public class IsMobileValidator implements ConstraintValidator<IsMobile, String> { |
使用自定义的注解进行参数校验:
1 | public class LoginVo { |
1 |
|
异常捕获
service层为业务逻辑层,该层方法返回值一定要是跟业务相关逻辑(一些异常情况的返回最好不要出现在这里),对该层的异常采取的是抛出全局异常的方式,然后定义一个全局异常处理器。如下
1 | public class GlobalException extends RuntimeException{ |
1 |
|
分布式Session
现实情况下会有多台服务器,用户的请求会落在不同的服务器上。
第一步:目标是用户登录成功后将用户标识信息token存入cookie可供后续获取
1 |
|
方便后续将token对应用户信息,,需要将用户信息存入第三方缓存中(redis)
redisService.set(MiaoshaUserKey.token, token, user);
1 |
|
第二步:根据cookie中token信息确定用户。完成将一个token映射成一个用户MiaoshaUser
上面两步就基本完成了分布式Session的概况。
不足:
有效期设置。通过token获取用户信息时添加一个延长有限期的方法,即重新设置一个token,并将用户信息存入缓存中。(上述代码已经改过了)
并不优雅。这里是实现商品列表页(
to_list
)的用户登录判断,会有一大串代码,再到商品详情页(/to_detail
)又会重新在验证一遍,很冗余。(上述代码已经改过了)
第三步:直接将MiaoshaUser对象传入方法参数中
这里方法是实现ArgumentResolver,SpringMVC中controller方法中会有很多参数,例如HttpServletResponse response, Model model
,这些参数的值是怎么来的?
就是由上面这个方法实现的。框架会回调这个方法向我们的controller方法中赋值。这里想将MiaoshaUser添加入参数中,就需要在List<HandlerMethodArgumentResolver>
中添加一个该类型的参数占住位置。UserArgumentResolver定义该参数,WebConfig中注入该参数。
1 |
|
1 | // 自定义这个参数 |
修改完成后的代码:
1 |
|
list
方法变得很清爽,业务逻辑全部由UserArgumentResolver
类中resolveArgument
方法实现,以后更改获取session的方法也只需要到这个方法中变更。
第三章小点
数据库设计
秒杀商品和秒杀订单两张表同其余两个表分开。
1 | -- 商品表 |
1 | -- 秒杀表 |
1 | -- 订单表 |
1 | -- 秒杀订单表 |
Vo类介绍
例如:LoginVo,表示的是表单提交信息的包装
1 | public class LoginVo { |
GoodsVo是对商品和秒杀商品的包装
1 | public class GoodsVo extends Goods{ |
这类包装类一般都是作为各层方法参数上的传输。
页面倒计时
1 | <script> |
秒杀执行
1 | <td> |
页面form表单的提交,里面的数据就是商品的ID(
goodsId
),提交到的路径是:/miaosha/do_miaosha
之后就再MiaoshaController里面完成
1 |
|
1 |
|
1 |
|
第四章小点
JMeter
并发测试:是指当并发达到多少时,网站的QPS是多少
QPS每秒查询率(Query Per Second)
每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 (看来是类似于TPS,只是应用于特定场景的吞吐量)
添加线程数
配置一个Http请求的默认值
配置http请求
添加结果
无任何优化的结果大概是在84的QPS
使用top
命令查看系统进程情况,load average参数已经严重过载,mysql进程占用CPU最多,所有的负载被其占尽。(说明瓶颈在这)
测试二:带参数的接口压测(新建一个获取用户信息的接口)
多用户时,可以添加一个配置文件,然后引用配置文件即可。
命令行测试
将程序打jar包:F:/IdeaProjects/miaosha>mvn clean package
分析一下这个包:
将jar包传到服务器上,开始运行:
nohup会将运行结果输入到当前文件夹下的nohup.out文件中。
查看nohup.out文件能看到程序运行结果
将Jmeter中商品列表的接口测试另存为生成一个.jmx
文件,上传到服务器中,开始在服务器中压测。
结果:
/goods_list接口、QPS1267、5000并发*10
/do_miaosha
接口测试
先生成Jmeter配置文件
同时将所需文件上传服务器,(将miaosha.jmx配置文件上传路径修改)执行
结果:
/do_miaosha
接口、QPS1161、5000并发*10
Redis压测
方式为使用redis自带的压测工具benchmark。第一条命令行含义为模拟100个并发,100000个请求。第二条命令标识更改为100字节测试(第一条命令时3字节),-p表示简化结果输出
结果:1秒能完成62695个请求
第三条测试命令:
表示只测试
set
和lpush
命令,同样是100000个请求。
第四条命令:
只测试指定的一条命令。
SpringBoot打war包
需求:将程序打成war包放到tomcat下运行。
方法:(pom引入两个依赖就好)
1是编译时依赖,2是war包插件
最后还要修改一下主方法:
打包:F:/IdeaProjects/miaosha>mvn clean package
完成后会生成文件:miaosha.war
放入tomcat文件加下:
启动后会有个路径问题
/miaosha
,可以直接将war包放入ROOT文件下,或者将/miaosha
部署为空。
附加:linux删除rm -rf file_name
;下载sz file_name
第五章-页面优化
页面级采用缓存
页面缓存+URL缓存+对象缓存
粒度:页面缓存 > URL缓存 > 对象缓存
页面缓存
商品列表为例:
原:thymeleaf模板渲染goods_list
,将数据放入model
中,然后加载渲染
1 | "/to_list") (value= |
改:将整个页面作为一个对象存入缓存
实现方式:第三方取页面缓存 ? 输出 : 手动渲染页面(使用SpringBoot框架自带)+ 存入第三方缓存中
1 | "/to_list", produces="text/html") (value= |
通常页面缓存有限期时间较短(60秒差不多了)
URL缓存
与页面缓存原理一样,页面缓存类似于一个商品列表页的缓存,而URL缓存类似于商品详情页的缓存。
同样对于商品详情页面的缓存
原:
1 | "/to_detail/{goodsId}") ( |
改:
1 | "/to_detail2/{goodsId}",produces="text/html") (value= |
对象缓存
与上面两个的页面和URL缓存不同,这里的粒度最细,对象级别。例如上述的分布式session实现:
1 | public MiaoshaUser getByToken(HttpServletResponse response, String token) { |
这里将修改秒杀用户获取方式:
原:
1 | public MiaoshaUser getById(long id) { |
实现方式:第三方取对象缓存 ? 输出 : 访问数据库取出 + 存入第三方缓存中
1 | public MiaoshaUser getById(long id) { |
注意点:当有对对象的修改操作发生时,同时需要对缓存的更新。且顺序一定是先更新数据库数据,再更新缓存数据。
这里涉及缓存和数据库数据一致性问题,最常用的逻辑如下:
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
为什么呢?因为如果是先修改缓存,再数据库,当缓存失效时发生读操作,缓存无效,从数据库中读取脏数据,再存入缓存,后数据库更新,就会产生数据不一致现象。
这里也可以解释,在service层中调用的一定是别的service而不是别的dao,因为在各个service层中可能会有缓存数据的存在。
1 | // 例如对对象密码进行修改时 |
这里将修改秒杀商品订单获取方式:
引入第三方缓存,执行秒杀验证用户是否重复秒杀时不直接从数据库中查找订单。
原:
1 | public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) { |
改:
1 | public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) { |
压测结果:QPS-2884
页面静态化,前后端分离
页面缓存与页面静态话的区别
页面静态化即所有页面都是纯HTML,通过JS和Ajax请求服务端拉取数据渲染页面。静态化后浏览器可以将HTML缓存在客户端,就不用重复下载页面数据,只需要下载动态数据即可,极大节省了网络流量。
而页面缓存仍是服务器端缓,仍需要从服务端下载数据,会耗费很大的流量。
实现,这里不用上面的技术实现,而是使用jquery模拟实现方式。
案例
案例1:使用的商品详情页面/to_detail/{goodsId}
。
页面只存HTML(纯HTML),动态数据通过接口和服务端获取。
1.先将商品详情信息重新封装为GoodsDetailVo类,在详情页面方法中传递,该方法体中没有页面渲染操作了:
1 | "/detail/{goodsId}") (value= |
2.页面修改
案例2:秒杀执行静态化/do_miaosha
。
页面只存HTML,动态数据通过接口和服务端获取。先将秒杀订单封装为MiaoshaOrderVo,传递到页面中。
附加:浏览器缓存(的时间定义)
Pragma :HTTP1.0
Expire :HTTP1.0&1.1 服务器标准时间(带时区)
Cache-Controller :HTTP1.0&1.1 客户端定义i时间(300秒)
原:
1 | "/do_miaosha") ( |
改:
1 | "/do_miaosha", method=RequestMethod.POST) (value= |
页面动态调用服务端接口获取数据
1 | <script> |
案例3:获取订单详情页面/order/detail
。
页面只存HTML,动态数据通过接口和服务端获取。先将秒杀订单封装为MiaoshaOrderVo,传递到页面中。
1 | "/detail") ( |
超卖问题
第一步:在减库存操作中修改Updat sql语句:
这是因为数据库会在Update执行时在执行语句的线程上加上锁,不会允许同时有多个线程执行。
第二部:设置同一用户不允许重买,即同一个用户秒杀到两个商品。
解决方法:利用数据库的唯一索引,在miaosha_order
表中调价一个由用户id+商品id
的唯一索引。
这样在创建订单的方法中,生成秒杀订单过程中如果有唯一索引冲突就会报错然后由于springboot的事务机制产生回滚。
总结:
静态资源优化
总结:并发问题的根源在于数据库的访问,最有效的解决方式时添加缓存,缓存的种类有很多,从用户的角度来说,从用户发起请求,在浏览器上可以通过页面的静态化将页面缓存在用户的浏览器端,在请求到达服务器之前,可以布置一些CDN节点,让请求访问CDN节点,再发来的请求访问时,可以在Nginx上添加缓存,再过来时可以在应用程序上添加缓存,即页面缓存。更细粒度的就是对象缓存,一直到最后的数据库缓存。
第六章-接口优化
服务级接口优化
总体思路:较少数据库访问。
RabbitMQ
安装
安装erlang
安装RebbitMQ
涉及一些linux命令行可供参考
集成
使用
配置
1 |
|
消息发送者
1 |
|
消息接收者
1 |
|
秒杀优化
目标:将系统的同步下单改为异步下单
- 商品库存加载
1 |
|
- redis预减库存
1 | "/do_miaosha", method=RequestMethod.POST) (value= |
这里再修改一下秒杀代码,
原代码:
1 |
|
修改后:
1 |
|
- 前端的代码修改
1 | <script> |
- 客户端轮询请求
1 | /** |
- 设置内存标记
1 | // 标记 |
Nginx简介
配置文件的主要参数
Nginx监听服务器80端口,将所有请求反向代理(
proxy_pass
)给http://server_pool_miaosha
,在http://server_pool_miaosha
中可以配置多个tomcat服务器接受请求,这样就实现了横向扩展。weight
参数表示的是请求分配的比重,即负载均衡的实现。max_fails
和fail_timeout
是一种服务器探活机制,
第七章-安全优化
秒杀接口隐藏
原因:因为我们的前端地址代码都是透明的,很容易受到攻击
- 前端代码修改
1 | <td> |
- 后端
1 | "/{path}/do_miaosha", method=RequestMethod.POST) (value= |
图形验证
- 前端,在秒杀进行中情况下显示图形验证码
1 | <div class="row"> |
- 后端对应验证码
1 | "/verifyCode", method=RequestMethod.GET) (value= |
- 验证
1 | "/path", method=RequestMethod.GET) (value= |
修改后的MiaoshaService
1 |
|
接口防刷
缓存记录用户访问次数进行限制
- 一般方式:在秒杀接口中加入如下代码
- 通用方式:注解拦截器
注解
1 | (RUNTIME) |
拦截器
1 |
|
这里修改了获取用户代码,修改前
1 |
|
修改后:
1 |
|
验证信息包装
1 | public class UserContext { |
注册
1 |
|
使用
1 | 5, maxCount=5, needLogin=true) (seconds= |