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封装(设计模式中的模板模式

    接口 <— 抽象类 <— 实现类

    1. 接口:规定一些契约
    2. 抽象类:实现一些共通的方法
    3. 实现类:进行一些特殊功能的实现

2. 实现登录功能

  • 数据库设计

  • 明文密码两次MD5处理

    1. 用户端:PASS = MD5(明文 + 固定Salt)

      防止用户明文密码在网络上进行传输

    2. 服务端:PASS = MD5(用户输入 + 随机Salt)

      防止数据库数据泄露,根据MD5值反推回明文密码

  • JSR303参数检验 + 全局异常处理器

    将在具体controller方法中进行的参数校验,转变为针对具体类属性的校验(JSR303的注解,可以自定义注解),当不满足注解要求时抛出异常。针对其他的一些校验处理定义全局异常,其中存储返回的CodeMsg信息,在service方法中进行校验判断并抛出对应信息的全局异常。定义全局异常处理器对这些异常进行捕获处理,并在其中根据不同的异常信息返回不同的错误信息。

    这样对参数的校验,就封装在一个独立的全局异常处理器当中,在 service方法中 或 根据JSR303注解 抛出异常即可,无需在controller方法中进行校验,减少了代码的冗余。

  • 分布式Session(同步不同机器上的Session信息)

    根据服务端,把一个token写入到cookie当中,客户端在随后的访问当中携带这个cookie,服务端就通过cookie和token就可以找到token对应的用户。

    1. 首先生成一个对应的UUID作为token,与要同步信息组成(K, V)对。
    2. (K, V)存入redis缓存中。
    3. 将token信息存入一个cookie中,并将cookie放入response中。
    4. controller请求方法可以通过@CookieValue@RequestParam来获取token值,并通过token从redis中取出同步信息的值。

    我们可以通过上述4中的方式,在controller方法的参数列表获取request、response、token等,在方法内进行处理。但是这样操作,在每个需要处理的方法中都会产生冗余、复杂的参数列表以及处理代码。因此,对处理代码进行封装,并将处理的结果作为 controller方法参数列表可获取的参数,就可以大大减少冗余代码。关键实现类WebMvcConfigurerAdapter,接口HandlerMethodArgumentResolver,方法resolveArgument

3. 实现秒杀功能

  • 数据库设计

  • 商品列表页

  • 商品详情页

  • 订单详情页

    dao中需要插入数据的同时返回信息,除了@Insert注解,还需使用@SelectKey注解,不能直接用返回值接收,而是执行完sql语句后,会将对应属性值赋给该对象,通过对象来获取属性值。

4. JMeter压测

  • JMeter入门

  • 自定义变量模拟多用户

  • JMeter命令行使用

    1. 在windows上录好jmx
    2. 命令行:sh jmeter.sh -n -t XXX.jmx -l result.jtl
    3. 把result.jtl导入到jmeter

    redis使用redis-benchmark进行测压

  • Spring Boot打war包

    1. 添加spring-boot-starter-tomcat的provided依赖
    2. 添加maven-war-plugin插件
    3. 启动函数继承SpringBootServletInitializer类,重写configure方法

5. 页面优化技术

  • 页面缓存 + URL缓存 + 对象缓存(粒度划分不同)

    • 页面缓存(商品列表页):这种缓存技术一般用于不会经常变动的信息,并且访问次数比较多的页面,这样就不用了每次都动态加载,缓存时间较短。
      1. 取缓存
      2. 手动渲染模板
      3. 结果输出
    • URL缓存(商品详情页):这里的URL 缓存相当于页面缓存 —— 针对项目中的详情页{goodsId},不同的详情页,显示不同缓存页面,缓存时间较短。URL缓存其实和页面缓存思路相同,二者的不同在于缓存中加入了URL。其实到这里URL缓存已经很好理解了,只需要在缓存的key中加上URL携带的参数就可以。
    • 对象缓存:相比页面缓存是更细粒度缓存 + 缓存 更新。对象缓存就是 当用到用户数据的时候,可以从缓存中取出。比如:更新用户密码
  • 页面静态化,前后端分离

    将页面缓存到客户的浏览器上,当用户访问页面的时候,直接不与服务器有交互,直接从本地缓存中拿取页面,节省网络流量。实际就是html + ajax。

    非静态化就是,请求发起后,controller方法中处理,并返回新的页面。

    而静态化,访问直接访问用户本地的缓存的html页面 (浏览器会缓存下来静态static下文件),静态资源,然后通过前端ajAx来访问后端,获取页面需要显示的数据返回即可。

    1. 常用技术AngularJS、Vue.js

    2. 优点:利用浏览器的缓存


    • 每一个秒杀订单创建后,存入redis缓存,在判断是否已经秒杀时,不需要访问数据库。
    • 为了防止一个用户同时发出多个秒杀请求,抢到多个商品,在miaosha_order中建立user_idgoods_id的唯一索引。
  • 静态资源优化

    1. JS / CSS 压缩,减少流量
    2. 多个 JS / CSS 组合,减少连接数
  • CDN优化,就近访问

解决超卖问题:

  1. 数据库加唯一索引:防止用户重复购买
  2. SQL加库存数量判断:防止库存变成负数

6. 接口优化

思路:减少数据库的访问

  1. 系统初始化,把商品库存数量加载到Redis

  2. 收到请求,Redis预减库存,库存不足,直接返回,否则进入3

  3. 请求入队,立即返回排队中

  4. 请求出队,生成订单,减少库存(访问数据库)

  5. 客户端轮询,是否秒杀成功


  • Redis预减库存减少数据库访问

  • 内存标记减少Redis访问

  • RabbitMQ队列缓冲,异步下单,增强用户体验

  • RabbitMQ安装与Spring Boot集成

    发布/订阅模式:生产者将消息发送到交换机(Exchange),再由交换机转发到队列,交换机只负责转发消息,不具备存储消息能力,如果没有队列和交换机绑定,或没有符合的路由规则,则消息会被丢失。交换机常见四种类型:

    1. Direct Exchange(定向)
    2. Topic Exchange(通配符)
    3. Fanout Exchange(广播)
    4. Headers Exchange((K,V))
  • 访问Nginx水平扩展

    扩展到多台服务器上,负载均衡配置

  • 压测

7. 安全优化

  • 秒杀接口地址隐藏

    思路:秒杀开始之前,先去请求接口获取秒杀地址

    1. 接口改造,带上 PathVariable 参数
    2. 添加生成地址的接口
    3. 秒杀收到请求,先验证PathVariable
  • 数学公式验证码

    思路:点击秒杀之前,先输入验证码,分散用户的请求

    1. 添加生成验证码的接口

    2. 在获取秒杀路径的时候,验证验证码

    3. ScriptEngine使用

      BufferedImage生成图片,随机生成数字和运算符号,组成表达式,写入图片,通过输出流传给前端。

      使用ScriptEngine直接可以计算,得到运算表达式的字符串的结果值。

  • 接口限流防刷

    思路:对接口做限流

    1. 可以用拦截器减少对业务入侵

细节总结

  • 在集成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赋值用textinput赋值用val

  • 进行多用户jmeter压测时,每次生成的token.txt,只能供当前的登录和启动使用,当重新登录或服务器重启后,需要重新生成token.txt,因为每次登录的token是由UUid随机生成的,然后将(token,user)存储在redis缓存中,并将token添加到cookie中。若服务器重启,或者用户重新登录,对应的token就会改变。

  • 进行压测时,虽然秒杀订单数不会变成负数,但是生成的订单和秒杀订单比规定的数量多

    可能是在处理并发的时候有些逻辑写的有问题,暂时未找到具体出问题的地方,后续进行接口优化后,测试结果正确。

  • 用rabbitmq传输MiaoshaMessage的json,MQReceiver端接收不到,普通的json却可以接收

    猜测可能与页面静态化,页面缓存等有关,或是redis服务器状态相关,将redis服务器重启,清空浏览器的缓存后,接收成功。


效果展示

  1. 登录界面

  2. 商品列表页

  3. 商品详情页

  4. 订单详情页


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!