基于SpringBoot、Redis、RocketMQ的秒杀系统设计

  • Sonihr秒杀系统设计
    • 写在前面
    • 新技术栈:消息中间件
      • 消息队列可以解决什么问题?
      • RabbitMQ
        • 如何安装
      • RocketMQ
        • 基本知识点概述
        • 官网样例说明
    • 新技术栈:Druid连接池
      • SpringBoot中配置Druid连接池
    • 新技术栈:Redis
      • 安装redis(windows和linux都可以安装)
      • SringBoot整合redis
      • RedisService与XxxKeyPrefix
    • 登录系统设计亮点
      • 全局处理Exception
      • 分布式session
    • 秒杀优化(缓存篇)
      • 缓存的模板流程
      • 页面缓存
      • 对象缓存
      • 页面静态化(html+ajax)
      • 静态资源优化手段
      • 解决超卖
      • 回顾
    • 秒杀优化(接口优化篇)
      • 总思路
      • redis预减库存
      • 内存标记
      • 异步下单
      • 数据库分库分表
      • 小结(一图流)
    • 秒杀优化(安全优化篇)
      • 总思路
      • 验证码与防止接口暴露
      • 接口防刷限流
    • 压测
      • 部署到Linux服务器中
      • Jmeter压测
      • 模拟多用户

Sonihr秒杀系统设计

写在前面

  1. 项目来源:

    https://github.com/Grootzz/seckill

  2. 对应课程

    https://coding.imooc.com/class/168.html

新技术栈:消息中间件

消息队列可以解决什么问题?

  1. 解决项目与项目之间消息传递和项目耦合的问题。具体而言,A项目向B项目传递数据,原来是通过A项目向B项目发送请求,等待返回数据,如果B项目挂了,那A项目就永久阻塞了。如果在A和B项目之间有一个MQ,那么A向MQ发送请求,B从MQ中获取请求,这样就解耦了,而且将AB之间的通讯变成了异步的。
  2. kafka基于scala开发,吞吐量特别高,队列堆积量很大,支持消费者长时间离线,一般用于日志。rocketMQ基于kafka思想,通过java开发,提供丰富的拉取模式(推送是MQ向消费者传递,拉取是消费者向MQ主动获取),支持事务消息,亿级消息堆积能力,吞吐量很高,比kafka低一点。RabbitMQ,基于erlang开发

RabbitMQ

如何安装

  1. https://blog.csdn.net/axela30w/article/details/81051287
    erlang可以在erlang中文网下载,官方网站非常缓慢。上面这篇博客的erlang安装过程没毛病,注意每次配置完成后要关闭当前cmd窗口再打开一个命令框,因为你后配置的环境变量对当前的黑框框命令行窗口来说没用,
    https://www.cnblogs.com/vaiyanzi/p/9531607.html
    rabbitMQ直接在官网下载即可,配置环境变量的方式如上问所示即可,均测试通过。然后先通过rabbitmq-plugins enable rabbitmq_management命令安装插件,然后rabbitmq-service start开启rabbitMQ服务,此时在浏览器地址栏输入http://localhost:15672/#/即可进入rabbitMQ的服务页面。

RocketMQ

基本知识点概述

  1. windows下命令行
    mqnamesrv.cmd -n localhost:9876
    mqbroker.cmd -n localhost:9876 autoCreateTopicEnable=true &
    mqshutdown.cmd broker
    mqshutdown.cmd namesrv
  2. 架构介绍
    基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第1张图片
    • nameServer 起到控制中心/路由的作用,主要负责注册其他组件,类似于Tomcat的Server。
    • broker 将broker的信息注册到name server中,负责消息处理,是MQ的核心
    • producer 从name server中找broker
    • consumer 类似上面
  3. 消息生产和消费介绍
    • 消息:普通消息、顺序消息、事务消息。顺序消息实现有序消费,事务消息可以解决分布式事务实现数据最终一致性。
    • 消费模式:1.Push,推送,consumer向broker发出请求,保持长连接,broker每5s检测一次是否有消息,如果有就推送给consumer。Broker会主动记录消息消费的偏移量。 2.Pull,拉取,定时获取,获取消息方便,负载均衡性能可控,但是消息的及时性差并且需要手动记录信息消费的偏移量。 但是原理都是consumer向broker请求,只是实现机制不同
    • RocketMQ发送消息会默认存储到4个队列中(数量可配置)以应对并发。
    • ACK机制:就是有一些应答状态的消息。
  4. 生产者逻辑:
    • 创建DefaultMQProducer
    • 设置NameSrvAddr
    • 创建一个Message,要指定Topic,tags用于消息过滤,keys是消息的唯一值,用于定位消息信息,body,需要用字节码
    • 用SendResult sendResult = producer.send(message) 发送消息
    • producer.shutdown() 关闭producer
  5. 消费者逻辑:
    • 创建DefaultMQPushConsumer
    • 设置NameSrvAddr
    • 设置subscribe,订阅要读取的主题信息consumer.subscribe(“topic”,subExpression:"*") 后面参数为过滤规则
    • 创建信息监听器MessageListener,consumer.registerMessageListener(new MessageListenerConcurrently{@override …})
    • 开启consumer,consumer.start()
  6. 顺序消息
    • 顺序消息是指按照消息的发送顺序来消费,rocketMQ保证分区(queue)有序,但是不保证全局有序。一个broker中有多个queue,因此无法保证全局有序。
      基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第2张图片
    • 解决方案:
      //消息会发送到队列1中,send的第三个变量arg表示发到下标为几的队列
      Message message = new Message("TopicTest" /* Topic */,
              "顺序" /* Tag */,"顺序信息".getBytes(RemotingHelper.DEFAULT_CHARSET));
      producer.send(message, new MessageQueueSelector() {
          @Override
          public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
              Integer index = (Integer)o;
              return list.get(index);
          }
      },1);
      
  7. 事务消息
    基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第3张图片

官网样例说明

  1. producer <----> MQ <----> consumer 同步和异步指的是发送者和MQ之间
  2. Send Messages Synchronously:可靠的同步传输,常常用于重要的 通知消息、短信通知等场景。(这边可以用于注册)
    • 原理:同步发送是指消息发送方发出数据后,会在收到接收方发回相应之后才发下一个数据包的通信方式。
      基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第4张图片
  3. Send Messages Asynchronously:异步传输,常用于时间敏感的业务场景
    • 原理:发送方发出数据后,不等待接收方发回相应,而是接着发下一个数据包。RocketMQ的异步发送需要用户实现异步发送回调接口(SendCallBack),这个接口规定了两个方法onSuccess和onException,规定了成功或异常时的处理方式,即当MQ收到数据后会异步回调这个接口的实现类,从而通知调用方请求的响应结果。
      基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第5张图片
  4. Send Messages in One-way Mode:单向传输用于可靠性中等的场景,比如日志收集
    • 原理:发送方只负责发送消息,不等待MQ服务器回应且没有回调函数触发,只请求不等待回应。发送消息的速度非常快,一般在微秒级别。但是相对而言可靠性就比较低。

新技术栈:Druid连接池

SpringBoot中配置Druid连接池

  1. SpringBoot为2.1.5,只需要在maven中导入如下依赖即可:

     
         com.alibaba
         druid-spring-boot-starter
         1.1.17
     
    
  2. 在application.properties中写入如下配置
    ```
    spring.datasource.username=xxxx
    spring.datasource.password=xxxxxxx
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/xxxxxxx?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8

     spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    
     #druid pool standard config
     spring.datasource.druid.max-active=30
     spring.datasource.druid.initial-size=3
     spring.datasource.druid.min-idle=3
     spring.datasource.druid.max-wait=12000
     spring.datasource.druid.time-between-eviction-runs-millis=60000
     spring.datasource.druid.min-evictable-idle-time-millis=30000
     spring.datasource.druid.pool-prepared-statements=true
     spring.datasource.druid.max-open-prepared-statements=30
    
     spring.datasource.druid.validation-query=select 1
     spring.datasource.druid.test-while-idle=true
     spring.datasource.druid.test-on-borrow=false
     spring.datasource.druid.test-on-return=false
    
     spring.datasource.druid.stat-view-servlet.enabled=true
     spring.datasource.druid.stat-view-servlet.login-username=xxxxx
     spring.datasource.druid.stat-view-servlet.login-password=xxxx
     spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
    
     spring.datasource.druid.web-stat-filter.enabled=true
     spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
     ```
    
  3. 上述配置分为三部分,1.JDBC连接用的 2.druid连接池的配置 3.druid监控需要配置一个servlet和一个filter。

  4. 但是很奇怪,我这样配置在IE里是正确的,但是在chrome中会反复跳转会login.html。

新技术栈:Redis

安装redis(windows和linux都可以安装)

  1. windows安装只有低版本的,高版本的在linux中,但是本地测试的时候可以先用windows环境测试。
  2. redis的配置有很多人都说了,我配置好了redis.conf配置文件,阿里云服务器也开启了了6379端口,但是无法通过校园网连接,怀疑是校园网防火墙禁用了这个端口。
  3. 未来肯定要用linux部署的,因为这个项目是分布式的,而不是单机了。

SringBoot整合redis

  1. https://blog.csdn.net/dingse/article/details/80572783

  2. 首先在pom中配置,starter配置redis,后一个配置jedis。

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
    dependency>
    

    然后要在application.properties中进行setter注入,这里的配置文件都会被注入到RedisProperties类中,然后这个类会被注入到容器中。

    #redis
    #redis数据库索引(默认为0)
    spring.redis.database=0
    #redis服务器IP地址
    spring.redis.host=localhost
    #redis端口号
    spring.redis.port=6379
    #redis密码,默认为空
    spring.redis.password=
    #连接redis超时时间(毫秒)
    spring.redis.time-out=0ms
    #jedis连接池
    ###############################
    #最大等待时间
    spring.redis.jedis.pool.max-wait=1000ms
    #最小空闲数量
    spring.redis.jedis.pool.min-idle=1
    #最大空闲数量
    spring.redis.jedis.pool.max-idle=10
    #最大连接数量
    spring.redis.jedis.pool.max-active=1000
    

    最后写一个RedisPoolFactory,利用RedisProperties来生成一个JedisPool实例,并且通过@Bean注入到容器中,注入到容器中的bean名称为方法名,类型为返回值类型。因为@Autowired是按照类型注入的,因此名字可以随意。

    @Service
    public class RedisPoolFactory {
    
        //自动注入redis配置属性文件
        @Autowired
        private RedisProperties properties;
    
        @Bean
        public JedisPool jedisPool(){
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxIdle(properties.getJedis().getPool().getMaxIdle());
            config.setMaxTotal(properties.getJedis().getPool().getMaxActive());
            config.setMaxWaitMillis(properties.getJedis().getPool().getMaxWait().toMillis());
            JedisPool pool = new JedisPool(config,properties.getHost(),properties.getPort(),100);
            return pool;
        }
    
    }
    

RedisService与XxxKeyPrefix

  1. RedisService类似慕课网那个免费秒杀项目中的redisDao,本质上就是写一个通用的redis交互类,类似于UserDao,ProductDao一样。脑补一下,作为缓存肯定要写set和get方法,但是因为redis中不能存对象,所以我们有两种处理方式1.将对象转成json以字符串形式保存。2.将对象序列化后以字符串形式保存。前者对人类友好但是相对而言速度慢,可以采用alibaba的fastJson实现,后者可以使用google的ProtoStuff实现快速序列化。本项目中目前还是转化成json。
  2. 第一步转化成json存在的问题是,首先我们要写两个工具方法,string转对象,对象转String。这两个方法都必须指定对象的Class类型,所以可以用泛型Class。
  3. 为了保证key的标识有意义且唯一,因此通过模板设计模式,KeyPrefix接口,BasKeyPrefix抽象类,OrderPrefix,UserPrefix…作为前缀,加上key成为真正的key,可以保证不同的业务用不同的key,不会相互干扰。最好在getPrexfix的时候加上className,这样可以尽量保证不同业务间的key不同。
    @Override
    public String getPrefix() {
        String simpleName = getClass().getSimpleName();
        return simpleName + ":" + prefix;
    }
    
  4. prefix中继承了过期时间的参数,在baseKeyPrefix中默认为0,不需要设置过期时间的采用父类默认构造方法即可,否则在构造方法中传入参数。在GoodsKeyPrefix中可以要先写几个static变量,作为预设的前缀。
  5. 在redisService中增加了一些常用的方法,exists,incr,decr,delete,判断是否存在,自增自减,删除。

登录系统设计亮点

全局处理Exception

  1. SpringBoot中会将所有的异常以统一的页面展示,用@ControllerAdvice注解一个全局异常Handler类,在该类中不同的方法用不同的@ExceptionHandler(xxxxException.class)注解,表示对某某异常进行拦截,然后在方法中传入参数为request和exception e,可以对e进行操作。本项目中是判断e的不同类型然后返回不同的值。
  2. 和mmall的项目一样,返回值要用一个复用的对象,本项目中用的是Result,包含msg,data和statusCode。如果正确则调用success返回data,否则返回CodeMsg,无论是T data还是CodeMsg,前端收到的都是json数据。

分布式session

  1. 因为分布式情况下,有多台服务器,会通过负载均衡服务器进行分配,因此要将session分布式保存。
  2. 因为服务器是分布式的,因此想到用redis保存,其中session_data表示本来user对象应该放在session中,但是因为是分布式session,因此要把user对象放到redis中。token是服务器生成的UUID,并且通过cookie交给浏览器缓存,缓存时间和redis的缓存时间一致,为2天。流程为:浏览器发起登录请求->服务器接受请求并到数据库中做密码比对->mybatis返回user对象->uuid生成token->放入redis中->将token放入cookie并返回给浏览器->浏览器再次发出请求时,当需要进行用户鉴权时,先从redis中获取user对象,如果获取不到,再穿透到数据库中查找。这一步是为了鉴权时,从session中获取user来判断是否有权限,还有必须你需要在页面上显示用户信息,每次都要从缓存里获取时,也要用到分布式session
  3. 仅仅谈登录而已的话,流程为:浏览器登录请求->服务器查redis->如果查到则返回user对象并比较user对象的密码和请求中的密码是否一致->如果没查到则穿透到数据库中,并且set进redis中->把token和user放入redis中
  4. 修改密码的时候,先插入数据库中,删除redis中原来的值,然后设置为新值。每次只要执行与token有关的方法,都重新生成cookie,重新生成token,重新插入redis中,这样可以更新token。
  5. 当我们在查看购物车的时候,需要取出session中的userid作为查询条件, 在本项目中就是要从redis中根据token取出user。所以每次我都要判断请求携带参数或cookie中是否有token,这样冗余代码太多了,我们可以用WebMvcConfiguration+HandlerMethodArgumentResolver组合来解决。其实本质上相当于是SpringMVC中在mvc.xml中配置一个handlermethodargumentResolver。通过代码可以看出,resolver实现了职责链的设计模式,类似springMVC中handlerAdapter的样子,先用support判断当前resolver是否可以解析,如果可以就进行解析,逻辑是拦截参数中有Uesr类的controller方法,如果是,则从request中获取cookie和parameter,然后从redis中找user,然后返回user实例。下面给出在SpringMVC中的配置方法
    
    
    <mvc:argument-resolvers>  
        <bean class="org.study.lyncc.web.resolver.CurrentUserHandlerMethodArgumentResolver"/>  
    mvc:argument-resolvers>  
    

秒杀优化(缓存篇)

缓存的模板流程

  1. 查询:先查缓存,缓存没有就查数据库,数据库查完后写入缓存。
  2. 插入:先插入数据库,再插入缓存。如果反过来,就失去了数据库持久化的保障。
  3. 更新:先更新数据库,然后让原缓存更新。否则,失去数据库持久化保障。
  4. 删除:先从数据库删除,再让缓存失效。否则,先让缓存失效,但是数据库还没删,如果有请求访问redis,会重新加载将要被删除的缓存,然后此时数据库删除了,但是缓存还未删除。
  5. 总结:要保证一切读都是先从缓存读,一起写都是先向数据库写。

页面缓存

  1. 对goodlist商品页面进行缓存,即不走渲染流程,而是直接手动渲染。(这一步因为我是前后端分离,因此我不用处理)
  2. 手动渲染页面,而不是交给springboot。适用于变化不大的数据,缓存时间比较短。

对象缓存

  1. 分布式Session,缓存user信息。
  2. 查找秒杀订单时候,查redis。当秒杀逻辑下单成功后,保存到redis中。

页面静态化(html+ajax)

  1. 静态页面是网页(html、htm)的代码都在页面中,不是用类似jsp的方式动态生成页面的。
  2. 什么意思?就是页面存的是HTML,动态数据通过接口从服务器获取,好处是不用动态生成页面而是直接返回htm、html页面,再用过ajax填充数据,类似于刷淘宝的时候,有的数据资源其实是没出来的,要等等才能出来,这是一个道理。否则如果要等界面渲染,用户就不能向后拖拽,要等到全部从服务器获取到之后再返回,
  3. 动态数据通过ajax获取后端发来的json数据然后解析,但是前端的其他内容都是静态的。
  4. 对商品列表页,商品详情页和订单页都做静态化处理,ajax异步获取服务端接口信息,这也是前后端分离的作用。

静态资源优化手段

  1. JS/CSS压缩,检索流量。(会减少控制之类的无用字符,webpack工具)
  2. 多个JS/CSS组合,减少连接数。(服务器支持,比如Tengine,将多个JS/CSS请求合并成一次请求。)代码层面上可以用一个js包括多个js,访问总js即可。
  3. CDN,将服务器数据缓存到全网的很多个CDN节点上,这样用户总是访问自己开销最小的节点。CDN:内容分发网络。

解决超卖

  1. 查询语句中加入update t_order set count = count -1 where … and count > 0,通过数据库保证非负。
  2. 解决一个用户秒杀两个商品,可以将(用户,商品)建立唯一索引,插入之前先查询数据库是否存在,如果存在则直接返回重复秒杀错误。也可以进行优化,第一次生成秒杀订单成功后,将订单存储在redis中,再次读取订单信息的时候直接从redis中读取即可。

回顾

  1. 瓶颈在哪里?数据库,如何解决?用库存
  2. 用户发起请求时,通过做页面静态化可以将应用缓存在客户的浏览器中。
  3. 请求到达网站之前,可以让请求首先访问CDN,获得最近的节点。
  4. Nginx等服务器也可以加缓存。
  5. 页面级缓存
  6. 对象级缓存
  7. 采用缓存必然会产生数据不一致,需要自己进行取舍。

秒杀优化(接口优化篇)

总思路

  1. 系统初始化时,把商品存库数量加载到redis中
  2. 当收到秒杀请求后,redis预减库存,库存不足则直接返回
  3. 秒杀成功的请求入rabbitMQ,立即返回“正在抢购页面…”,当异步下单成功后才返回订单。
  4. 客户端轮询是否秒杀成功,服务器请求出队,生成订单,减少库存。

redis预减库存

  1. 优化原因:进一步减少对数据库的访问
  2. 初始化SeckillController时,实现InitializingBean接口,并实现setProperties方法,在这个方法中查询一次mysql数据库,并且把所有的商品信息都写入缓存中。缓存中存储了初始数据,那么之后的减少都可以在redis中完成。

内存标记

  1. 优化原因:减少对redis的访问,因为redis有网络开销
  2. 用hashmap来存储goodId的状态,是否售罄。因为hashmap是非线程安全的,所以我们根本来说是用数据库来保证一致性的,所以对hashmap的线程安全要求没那么高。比如redis已经减少到0了,此时还有100个线程,那比如有50个线程中因为线程标记而减少了对redis的开销,就已经达到了目的。

异步下单

  1. 优化原因:请求先入队列缓冲,再异步下单,增强用户体验
  2. 利用RocketMQ的同步模式,因为对交易而言对实时性要求要让步于可靠性。当用户下单时,直接给用户返回下单成功,但是其实真正的下单工作是交给MQ的接受者来完成的,比如生成订单,支付等复杂费时的操作都可以异步的完成。
  3. 客户端可以查询当前订单状态,或者由服务器向客户端发送邮件/短信通知异步处理的结果。

数据库分库分表

  1. 推荐中间件:阿里巴巴开发的mycat

小结(一图流)

基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第6张图片

秒杀优化(安全优化篇)

总思路

  1. 防止接口暴露。
  2. 验证码。1. 防止刷接口 2. 可以分散用户的请求,即错峰。
  3. 接口限流。

验证码与防止接口暴露

  1. 验证码和秒杀接口是对应的。什么意思呢?试想一下,假如开放秒杀后就暴露出秒杀接口,那就有可能有黑客刷秒杀接口,因为在开放秒杀后接口就是唯一的了。但是加上验证码之后,每次验证码提交后都会随机生成一个与用户名、商品、随机UUID相关的秒杀接口,每个人每一次的秒杀接口都不同,因此避免了刷接口。
  2. 首先访问miaosha/verifyCode?goodsId=1,服务器根据goodsId、user生成一个验证码,并将验证码的计算结果放到redis中。然后访问miaosha/path?goodsId=1&verifyCode=xxx,如果verifyCode与缓存中一致,则返回一个秒杀地址,秒杀地址是UUID随机生成的,存放在redis中,key为前缀+userId+goodsId。
  3. 生成验证码的过程是生成一张图品,BufferdImage类,Graphic类用于绘图,包括背景、边框、噪声、字符串。字符串是计算表达式,将计算表达式exp用ScriptEngine的eval方法可以直接计算出结果,或者利用算法将中缀表达式转化为后缀表达式(数字直接输出,符号(加减乘除左括号)入栈,输出所有优先级小于等于当前入栈符号的符号,遇到右括号则输出至左括号,左括号弹出但不输出。),然后计算结果。补充知识:后缀转中缀->先把后缀转换为树,在中序遍历一次

接口防刷限流

  1. 基础版本:redis中保存key为uri+IP,每次访问的时候都记录访问的真实IP。这里IP的获取如果没有配置反向代理可以直接从request.getRemoteAddr来获取。如果用Nginx配置了反向代理需要在配置中配置保留代理前的IP地址

    https://blog.csdn.net/b1303110335/article/details/77173548
    https://blog.csdn.net/youanyyou/article/details/79406454

    然后通过通过request的getHeader方法获取正确的IP。
    基于SpringBoot、Redis、RocketMQ的秒杀系统设计_第7张图片
    给定redis的这个键值对一个过期时间,比如5s,那么在5s内访问超过指定次数的就会被拒绝访问,5s之后缓存失效又会重新放入缓存中,这样每5秒每个IP就有指定次数的机会。(redis采用惰性过期+定期过期的过期策略,在秒杀环境下,一个用户经常多次刷新,因此采用惰性过期比较适合。)

  2. 进阶版本:

    • 通过拦截器,可以减少对业务代码的侵入。继承HandlerInterceptor接口,实现preHandle方法,内部完成redis缓存的相关逻辑。
    • 通过注解实现配置,不依靠配置文件。拦截器获取方法上的注解,通过注解找到redis缓存多少秒,多少次,是否需要登录。登录需要从request的cookie或者url中获取uesr。
    • 通过ThreadLocal保存user对象,因为servlet是单实例多线程的,一次请求就是一个线程,在这次请求中所有从UserContext中get的user都是同一个且线程安全的、

压测

部署到Linux服务器中

  1. 先在Linux服务器中的mysql中建立seckill表,注意这边要把sql的时间更改掉,改成2019年,不然无法秒杀。
  2. 如何把Springboot中的项目打包成war包?分为两步,第一步在pom.xml中加入依赖和插件。
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-tomcatartifactId>
        <scope>providedscope>
    dependency>
    
    <plugin>
        <groupId>org.apache.maven.pluginsgroupId>
        <artifactId>maven-war-pluginartifactId>
        <configuration>
            <failOnMissingWebXml>falsefailOnMissingWebXml>
        configuration>
    plugin>
    
    然后在MainApp中添加代码:
    @SpringBootApplication
    public class MainApp extends SpringBootServletInitializer {
    
        public static void main(String[] args) {
            SpringApplication.run(MainApp.class, args);
        }
    
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
            return builder.sources(MainApp.class);
        }
    }
    
  3. 源程序没有context,当我们部署到服务器tomcat时会产生问题,比如我们打包的war包是叫seckill,我们把seckill.war放到tomcat的webapps中,浏览的url应该是域名:端口/seckill/xxx。我们在application.properties中加上一句server.servlet.context-path=/seckill,相当于是指定了上下文为seckill,其他都不必改。

Jmeter压测

  1. 阿里云学生版服务器走mysql查询大概是80QPS,走redis查询大概是200QPS,写了一个脚本来记录top命令,发现CPU和MEM占用率都在50%左右,那瓶颈是什么呢?后来发现,服务器的带宽是1M的,无法支持大规模并发,因此转而在本机实现。
  2. 本机瓶颈在CPU,因为本机既要做压测,又要充当服务器,实测下来mysql和redis大概都是800到1000的QPS。

模拟多用户

  1. 不仅数据库中要有多个用户,在redis中还要有多个token。
  2. 写循环insert5000个用户进表汇总,包括手机、姓名。然后用HttpClient类给服务器发送login请求以获取token,并将token写入本地文件中,这个文件就是压测时的csv文件,其中写入了userId和token。

你可能感兴趣的:(Java学习,JAVA面试复习笔记)