Java高性能高并发秒杀系统
本项目从零实现了一个秒杀系统的一些核心功能。
通过本项目能够学习如何应对大并发、如何利用缓存、如何使用异步、以及如何编写优雅的代码
关于项目的代码实现,请移步:代码
系统开发环境以及版本
- 操作系统:Windows_10(代码开发)、Centos7(Redis、RabbitMQ部署)
- 集成开发工具:IntelliJ IDEA 2020.2.1
- 编译环境:JDK_1.8
- 数据库:MySQL_5.7
技术点介绍
前端 | 后端 | 中间件 |
---|---|---|
Thymeleaf | SpringBoot | RabbitMQ |
Bootstrap | JSR303 | Redis |
JQuery | MyBatis | Druid |
业务逻辑
项目结构设计及实现功能
1. 项目框架搭建
Spring Boot环境搭建
集成Thymeleaf,Result结果封装
集成Mybatis + Druid
报错:Loading class com.mysql.jdbc.Driver’. This is deprecated`警告处理,jdbc更新。
处理:处理:提示信息表明数据库驱动
com.mysql.jdbc.Driver
已经被弃用了、应当使用新的驱动com.mysql.cj.jdbc.Driver
。所以,按照提示更改jdbc.properties配置com.mysql.jdbc.Driver
改为com.mysql.cj.jdbc.Driver
集成Jedis + Redis安装 + 通用缓存Key封装(设计模式中的模板模式)
接口 <— 抽象类 <— 实现类
- 接口:规定一些契约
- 抽象类:实现一些共通的方法
- 实现类:进行一些特殊功能的实现
2. 实现登录功能
数据库设计
明文密码两次MD5处理
用户端:PASS = MD5(明文 + 固定Salt)
防止用户明文密码在网络上进行传输
服务端:PASS = MD5(用户输入 + 随机Salt)
防止数据库数据泄露,根据MD5值反推回明文密码
JSR303参数检验 + 全局异常处理器
将在具体controller方法中进行的参数校验,转变为针对具体类属性的校验(JSR303的注解,可以自定义注解),当不满足注解要求时抛出异常。针对其他的一些校验处理定义全局异常,其中存储返回的CodeMsg信息,在service方法中进行校验判断并抛出对应信息的全局异常。定义全局异常处理器对这些异常进行捕获处理,并在其中根据不同的异常信息返回不同的错误信息。
这样对参数的校验,就封装在一个独立的全局异常处理器当中,在 service方法中 或 根据JSR303注解 抛出异常即可,无需在controller方法中进行校验,减少了代码的冗余。
分布式Session(同步不同机器上的Session信息)
根据服务端,把一个token写入到cookie当中,客户端在随后的访问当中携带这个cookie,服务端就通过cookie和token就可以找到token对应的用户。
- 首先生成一个对应的UUID作为token,与要同步信息组成
(K, V)
对。 - 将
(K, V)
存入redis缓存中。 - 将token信息存入一个cookie中,并将cookie放入response中。
- controller请求方法可以通过
@CookieValue
或@RequestParam
来获取token值,并通过token从redis中取出同步信息的值。
我们可以通过上述4中的方式,在controller方法的参数列表获取request、response、token等,在方法内进行处理。但是这样操作,在每个需要处理的方法中都会产生冗余、复杂的参数列表以及处理代码。因此,对处理代码进行封装,并将处理的结果作为 controller方法参数列表可获取的参数,就可以大大减少冗余代码。关键实现类
WebMvcConfigurerAdapter
,接口HandlerMethodArgumentResolver
,方法resolveArgument
。- 首先生成一个对应的UUID作为token,与要同步信息组成
3. 实现秒杀功能
数据库设计
商品列表页
商品详情页
订单详情页
dao中需要插入数据的同时返回信息,除了
@Insert
注解,还需使用@SelectKey
注解,不能直接用返回值接收,而是执行完sql语句后,会将对应属性值赋给该对象,通过对象来获取属性值。
4. JMeter压测
JMeter入门
自定义变量模拟多用户
JMeter命令行使用
- 在windows上录好jmx
- 命令行:sh jmeter.sh -n -t XXX.jmx -l result.jtl
- 把result.jtl导入到jmeter
redis使用redis-benchmark进行测压
Spring Boot打war包
- 添加spring-boot-starter-tomcat的provided依赖
- 添加maven-war-plugin插件
- 启动函数继承
SpringBootServletInitializer
类,重写configure
方法
5. 页面优化技术
页面缓存 + URL缓存 + 对象缓存(粒度划分不同)
- 页面缓存(商品列表页):这种缓存技术一般用于不会经常变动的信息,并且访问次数比较多的页面,这样就不用了每次都动态加载,缓存时间较短。
- 取缓存
- 手动渲染模板
- 结果输出
- URL缓存(商品详情页):这里的URL 缓存相当于页面缓存 —— 针对项目中的详情页{goodsId},不同的详情页,显示不同缓存页面,缓存时间较短。URL缓存其实和页面缓存思路相同,二者的不同在于缓存中加入了URL。其实到这里URL缓存已经很好理解了,只需要在缓存的key中加上URL携带的参数就可以。
- 对象缓存:相比页面缓存是更细粒度缓存 + 缓存 更新。对象缓存就是 当用到用户数据的时候,可以从缓存中取出。比如:更新用户密码
- 页面缓存(商品列表页):这种缓存技术一般用于不会经常变动的信息,并且访问次数比较多的页面,这样就不用了每次都动态加载,缓存时间较短。
页面静态化,前后端分离
将页面缓存到客户的浏览器上,当用户访问页面的时候,直接不与服务器有交互,直接从本地缓存中拿取页面,节省网络流量。实际就是html + ajax。
非静态化就是,请求发起后,controller方法中处理,并返回新的页面。
而静态化,访问直接访问用户本地的缓存的html页面 (浏览器会缓存下来静态static下文件),静态资源,然后通过前端ajAx来访问后端,获取页面需要显示的数据返回即可。
常用技术AngularJS、Vue.js
优点:利用浏览器的缓存
- 每一个秒杀订单创建后,存入redis缓存,在判断是否已经秒杀时,不需要访问数据库。
- 为了防止一个用户同时发出多个秒杀请求,抢到多个商品,在
miaosha_order
中建立user_id
和goods_id
的唯一索引。
静态资源优化
- JS / CSS 压缩,减少流量
- 多个 JS / CSS 组合,减少连接数
CDN优化,就近访问
解决超卖问题:
- 数据库加唯一索引:防止用户重复购买
- SQL加库存数量判断:防止库存变成负数
6. 接口优化
思路:减少数据库的访问
系统初始化,把商品库存数量加载到Redis
收到请求,Redis预减库存,库存不足,直接返回,否则进入3
请求入队,立即返回排队中
请求出队,生成订单,减少库存(访问数据库)
客户端轮询,是否秒杀成功
Redis预减库存减少数据库访问
内存标记减少Redis访问
RabbitMQ队列缓冲,异步下单,增强用户体验
RabbitMQ安装与Spring Boot集成
发布/订阅模式:生产者将消息发送到交换机(Exchange),再由交换机转发到队列,交换机只负责转发消息,不具备存储消息能力,如果没有队列和交换机绑定,或没有符合的路由规则,则消息会被丢失。交换机常见四种类型:
- Direct Exchange(定向)
- Topic Exchange(通配符)
- Fanout Exchange(广播)
- Headers Exchange((K,V))
访问Nginx水平扩展
扩展到多台服务器上,负载均衡配置
压测
7. 安全优化
秒杀接口地址隐藏
思路:秒杀开始之前,先去请求接口获取秒杀地址
- 接口改造,带上 PathVariable 参数
- 添加生成地址的接口
- 秒杀收到请求,先验证PathVariable
数学公式验证码
思路:点击秒杀之前,先输入验证码,分散用户的请求
添加生成验证码的接口
在获取秒杀路径的时候,验证验证码
ScriptEngine使用
BufferedImage生成图片,随机生成数字和运算符号,组成表达式,写入图片,通过输出流传给前端。
使用ScriptEngine直接可以计算,得到运算表达式的字符串的结果值。
接口限流防刷
思路:对接口做限流
- 可以用拦截器减少对业务入侵
细节总结
在集成Mybatis和Druid时,报错:
Loading class com.mysql.jdbc.Driver'. This is deprecated
警告处理,jdbc更新。提示信息表明数据库驱动
com.mysql.jdbc.Driver
已经被弃用了、应当使用新的驱动com.mysql.cj.jdbc.Driver
。所以,按照提示更改jdbc.properties配置com.mysql.jdbc.Driver
改为com.mysql.cj.jdbc.Driver
td
赋值用text
、input
赋值用val
。进行多用户jmeter压测时,每次生成的token.txt,只能供当前的登录和启动使用,当重新登录或服务器重启后,需要重新生成token.txt,因为每次登录的token是由UUid随机生成的,然后将(token,user)存储在redis缓存中,并将token添加到cookie中。若服务器重启,或者用户重新登录,对应的token就会改变。
进行压测时,虽然秒杀订单数不会变成负数,但是生成的订单和秒杀订单比规定的数量多
可能是在处理并发的时候有些逻辑写的有问题,暂时未找到具体出问题的地方,后续进行接口优化后,测试结果正确。
用rabbitmq传输MiaoshaMessage的json,MQReceiver端接收不到,普通的json却可以接收
猜测可能与页面静态化,页面缓存等有关,或是redis服务器状态相关,将redis服务器重启,清空浏览器的缓存后,接收成功。
效果展示
登录界面
商品列表页
商品详情页
订单详情页
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!