商城秒杀:原理与实战

Jmeter压力测试

  我们的服务器,页面能承担多少数据来访问,就需要用到压力测试,就是模拟实际用户使用情况,让软件在长时间超大负荷的情况下进行测试,来检测系统的性能、可靠性。用Jmeter来实现。
  Jmeter安装好后,进入Jmeter的可视化界面。所有的测试计划都是在TestPlan中制定。点击一个线程组来模拟用户。然后在里面设定用户量,每个用户发出多少次Http请求。然后配置HTTP请求的基本信息。
在这里插入图片描述
  点击运行后想要看到结果,还需要在Jmeter中创建一个listener表,即summary Report,在这个聚合报表中就会出现我们发送的统计信息。其中有一个参数就是吞吐量,也就是你服务器每秒能处理多少个请求。
在这里插入图片描述
  最小14毫秒可以看到结果,最长6315毫秒看到结果,平均等待418毫秒看到结果,之所以是这样是由底层设计造成的。205的吞吐量,也就是400个人,每个人发1个请求,需要2秒才能解决。可以看到,这里最长等待时间太长和吞吐量太低。

稳定数据优化—处理平时不怎么变更的数据进行优化

  在高并发环境下,大部分都是读取数据的,所以数据库最有可能成为高并发的瓶颈,因此提高数据库效率或者较低数据库交互式首要考虑的问题。在电商中,很大一部分数据是在一段时间内稳定不变的,例如商品信息、会员信息等,对于稳定数据,常有两种方式进行高并发处理:1.利用缓存(redis、memcached) 2.利用静态化技术,转化成html(因为动态页面会有一个服务器进行页面渲染过程,会消耗资源,我们把一次访问的结果转成HTML静态页面进行存储,下次访问都访问静态页面,从而降低服务器压力)。
  在代码中,我们让商品信息的数据进行缓存处理,因为商品是相对固定的。配置文件配置好后,用cacheable注解。该注解的含义是将第一次访问的时候将方法的返回结果放入缓存,第二次访问时不再执行方法内部的代码,而是从缓存中直接提取数据。Cacheable需要两个参数,一个key,一个value。
在这里插入图片描述
  Cacheable又叫做声明式缓存。
  Redis也有可视化管理控制器,他有16个数据库。我们要把对象放入redis缓存,必须要把这个对象序列化,也就是这个对象必须实现serializable。这样,当我们第一次访问网页的时候比较i慢,第二次访问的时候就会快很多。(另外几个是因为上面代码我没截图完)
在这里插入图片描述
  此时跑了一次后我们再用Jmeter进行压力测试,会发现吞吐量好了很多。

静态化技术

  动态页面便于管理,但是访问网页需要服务器进行页面渲染导致消耗资源,而静态页面访问速度快,但不易于管理,因此静态化就是把两者结合在了一起。也就是一次访问把动态页面渲染后变成静态页面存储下来,之后的访问就访问静态页面了。也就是让nginx去访问静态页面。
在这里插入图片描述
  要实现静态化技术,首先就要编写静态化处理代码(输出静态处理的页面地址):把返回的结果对象存放在Hashmap中,然后通过process进行生成静态页面。
在这里插入图片描述
  通过模板对象的process方法,该方法里面有两个参数,第一个参数是数据,第二个参数是哪里输出。FreemarkerConfig就是我们的容器。

  现在我们文件中有这么一个HTML静态页面了,但是现在我们生成的html没有样式,把样式复制进文件夹就可以了。
  现在静态页面有了,接下来要做的就是用Nginx与之进行绑定。Nginx是一款轻量级的web服务器和反向代理服务器,内存占用少并发能力强。把Nginx安装好后,打开Nginx,Nginx的端口号是80,Nginx打开只需要直接运行exe文件就可以了,关闭用命令行nginx –s stop就可以了。Nginx的核心配置文件是Nginx.conf,我们在核心配置文件中增加访问地址:
在这里插入图片描述
  这样我们在网页输入localhost/goods/1333.html 就可以访问了。Nginxx的地址也是Localhost。

自动静态化处理—针对量级很大的情况

  因为我们页面不可能一直处于静态的情况,可能会出现商品更新,此时就需要重新静态化,对面量级很大的静态化不可能重新把所有数据进行重新静态化,就需要用到自动静态化处理。
  自动静态化处理依赖于任务调度:启动任务调度的注释@EnableScheduling。现在正在编写任务调度代码:
在这里插入图片描述
  用@Scheduled注解表示我要按照指定的时间来执行我们的代码,cron=”* * * * * ?”即按照每秒钟执行一次,cron表达式来规定执行时间,代表秒 分 时 日 月 星期。有了任务调度后,我们可以规定把规定时间内例如5分钟修改的数据进行静态化,就减少服务器压力很多。要这样设置,我们就需要在goods表中添加一个最近更新时间的行,然后在程序中获取5分钟内修改过的数据对象,把这些对象进行重新静态化就可以了。
(获取最近五分钟修改过的数据)

动静数据分离

  在一个网页中,除了有静态数据,还应该有动态的经常变更的数据,例如评论,所以我们需要对这些静态和动态数据进行区别对待。对于这种动态数据就需要在静态页面中使用AJAX动态加载后端产生的数据。当我们HTML用AJAX发出请求的时候,注意是向我们的NGINX发出的请求,而不是向后台Tomcat发出请求,Nginx作为代理服务器转交给Tomcat去Mysql中取数据。这里注意的是,所有的请求和交互都是通过Nginx进行处理的。
在这里插入图片描述
  在数据库中生成一张评论表。对于前端代码参考网站layui。因为要用到Nginx所以需要在Nginx的配置文件nginx.conf中进行配置代理的是谁。

秒杀活动-超卖现象

  秒杀是最常见的高并发,且是瞬时的高并发。在秒杀中,最常见的挑战是为了避免超卖,即如何避免购买商品的人数不超过商品数量上限。现在我们模拟这个过程。
Dao层:
在这里插入图片描述
Service层:
在这里插入图片描述
Web层:
在这里插入图片描述

  编写好后,我们网页输入多次刷新是没有问题的,但是一旦我们用Jmeter模拟高并发的场景,就会报错了,因为会出现线程安全,此时你会发现购买的商品就超卖了,因为当多个人同时访问代码的时候,可能同时都拿到的是10,然后同时减1,变量是9,但是却卖了多个。这个时候就要用到redis缓存来进行互存预减的方式解决。

用redis解决超发问题

  之所以用redis解决超发问题,是因为1.redis是单线程模型,redis操作的所有指令都会以队列的形式在单线程中处理,早来的早处理,不会出现同时被处理的情况。2.redi是内存存储,效率高。3.天生分布式支持。解决思路如下:

①活动开始
  从活动商品列表中找到正在进行秒杀活动的商品,可以用任务调度定时扫描秒杀活动表,然后找到要进行的活动后用Redis中提供的List列表类型,我们把十个商品放入List当中作为商品库存,因为redis是单线程的,所以不用担心之前出现的多个用户访问的线程安全的问题。

②抢购过程
  Redis中还提供了一个set类型,当一个用户买了商品后,该商品从List当中弹出,然后把用户加入到set类型里。之所以用Set是因为set元素唯一,避免了同一个用户同时秒杀多个。
在这里插入图片描述
③编写任务调度删除在活动商品表中已经结束了的活动商品
  现在编写代码:
在这里插入图片描述
  先定义了一个要秒杀的商品对象Promotionseckill,也把该对象在数据库中新建了一个表。在springboot中提供了操作redis的模板类,叫做RedisTemplate,我们要用redis操作就要先注入RedisTemplate。Redis的使用之前也需要配置配置文件哈。
  在进行商品list的注入的时候,是必须在这个秒杀商品是在活动时间内为前提的,所以会先一个findUnstartSeckill()方法返回在规定时间内的秒杀商品,任务调度是在0秒的时候开始接下来的0秒每一秒执行一次。插入之前还需要删除以前重复的活动任务。RedisTemplate对象他会默认使用jdk的序列化方式帮我们把对象序列化。
  现在编写抢购过程的代码。先讲讲为什么之前不用redis时的超发出现原因,因为之前的程序中都是获取当前的库存,因为获取库存和更新库存有时间差,在高并发的环境下就可能出现超发。而在redis里面,通过预减过程,如果商品从List弹出成功,则把获取商品的用户加入set,当弹出完再次弹出则返回null,遇到null就不会再把用户加入set了。
在这里插入图片描述
  OpsForList()是针对这个List集合的,leftPop()是左侧弹出。通过responsseBody注解来给客户端把map结构转成json格式的字符串返回return。
在这里插入图片描述

  一个用户不能抢购多次,所以还需要有一个去重处理,即在service层判断同一个用户是否多次抢购到。否则我们的set里面就不是10个了,这里就体现了为什么用set,因为set不允许重复元素的出现,可以根据这个特性来进行去重判断,判断set中是否已经拥有了这个用户。
在这里插入图片描述

利用MQ进行订单流量的削峰限流

  之所以需要MQ进行削峰限流,是因为前台业务和后台业务处理能力不对称造成的。首先,秒杀用户前面已经讲过用redis缓存的方式进行确权得到哪些用户可以购买,可以购买的用户都存放在redis的set集合里,对于redis而言处理能力很快,每秒可以处理1000个订单,但是后台支付订单处理能力就很慢了,因为可能会和多个系统进行对接,那么每秒可能只能处理200个订单,这样当1000个用户同时给后台服务器处理的时候,就会出现问题。所以传统同步处理方式不可取,我们不可能让用户提交订单后一直等待后台服务器把其他业务处理完了然后再来处理你的订单,此时就需要异步处理。
在这里插入图片描述

  此时,一秒钟1000个用户都给MQ,消费者每次只从MQ中提取200个,从而预防宕机。
在这里插入图片描述

  现在模拟这个过程,先创建一张用户订单表,然后把RabbitMQ配置进spring boot中。然后把我们前台页面推送的订单编号给rabbitmq,处理订单的业务逻辑就根据自己的处理能力去MQ中拿取订单进行处理返回,但这里返回的并不是处理结果,而是订单编号OrderNo,这个订单编号是用来进行信息检索判断订单是否创建成功的,当返回订单编号后可以隔几秒钟再次发送一个checkorder去检查这个订单是否已经创建好了,如果没创建成功则继续等待,直到创建成功。
在这里插入图片描述
  现在模拟代码:要使用RabbitMQ,需要把rabbitTemplate注入进去,这个模板对象封装了rabbitm的一系列的操作方法。
①Service层
  把用户id和随机生成的订单号放入map,然后把map放入MQ中。
ConvertAndSend有三个参数,第一个是交换机名字,第二个是路由件为Null,第三个是数据data。返回订单信息oorderNo给web层打印输出。

②现在给一个web层代码
  凡是能进入sendOrderToQueue代码的用户,都是在redis的set中的拥有购买权限的用户,因为不是得话就进入catch了。
在这里插入图片描述

③在rabbitmq的可视化管理中localhost:15672,创建好我们的mq队列以及交换机。
④订单处理业务,从MQ中抽取订单来进行处理。在操作之前,需要配置定义消费者最多同时处理多少个消息,这里是10个。
在这里插入图片描述
  然后用RabbitListener注解来获取到我们配置的MQ,包括队列和交换机信息。

@Component
public class OrderConsumer {
    @Resource
    private OrderDAO orderDAO;

    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "queue-order") ,
                    exchange = @Exchange(value = "exchange-order" , type = "fanout")
            )
    )
    @RabbitHandler
    public void handleMessage(@Payload Map data , Channel channel ,
                              @Headers Map<String,Object> headers){
        System.out.println("=======获取到订单数据:" + data + "===========);");
        try {
            //对接支付宝、对接物流系统、日志登记。。。。
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Order order = new Order();
            order.setOrderNo(data.get("orderNo").toString());
            order.setOrderStatus(0);
            order.setUserid(data.get("userid").toString());
            order.setRecvName("xxx");
            order.setRecvMobile("1393310xxxx");
            order.setRecvAddress("xxxxxxxxxx");
            order.setAmout(19.8f);
            order.setPostage(0f);
            order.setCreateTime(new Date());
            orderDAO.insert(order);
            Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
            channel.basicAck(tag , false);//消息确认 
            System.out.println(data.get("orderNo") + "订单已创建");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  basicAck有两个参数,第一个参数是channel的id号,第二个参数是false表示不进行批量接收。

⑤检查订单是否创建好
  因为创建订单有一个过程,当我们把订单发送给MQ时因为MQ是异步处理业务所以很快就会返回一个订单编号(service层把订单给MQ后直接返回了订单编号,不用管MQ过程),前端拿到订单编号后不代表数据库中就有订单了(订单就创建好了),因为订单处理业务可能还需要连接其他端口等很慢,所以需要在ajax中调用web层方法查询订单是否创建好(ajax中可以设定每几秒钟调用一次方法),否则就一直处于等待页面。

Nginx负载均衡

  之前的所有秒杀操作,都是在单台电脑上完成的,单台电脑的处理能力是有极限的,往常是需要多台服务器来处理,也就要用到Nginx负载均衡。负载均衡就是把任务分配到多个服务器上分别执行进而达到提高执行效率缩短执行时间的目的,负载均衡有两种:硬负载均衡、软负载均衡。
  硬件负载均衡常见的f5,是封装好的硬件专门负责负载均衡的。软负载均衡就是我们用一台电脑下载好负载均衡的软件例如Njinx来实现负载均衡,常用于web领域。
  负载均衡的算法有:轮询策略(默认)、权重策略、最少连接数、IP绑定策列(源地址散列算法)、随机策列(random)、按响应时间(第三方,响应时间最短的优先)
  例如,我们在代码中模拟这么一个过程:
  我用四个不同的端口启动四份不同的服务器,那么我在访问的时候就很麻烦,因为我就需要选择是访问Localhost:8001/goods?gid=1335还是Localhost:8002还是8003还是8004,此时就需要用Nginx来帮忙分配。

首先安装好Nginx,然后再Nginx.conf核心配置文件中进行配置。

①Upstream 用来配置后端服务器池。
在这里插入图片描述
②接下来配置端口的转发和映射(转发策略),用server。Location指的是对哪些进行映射,斜杠代表全部。
在这里插入图片描述

③然后在命令行输入:start nginx.exe启动nginx。
  现在我们用80端口进行请求转发结果如下(默认的80端口,可以不用写就默认):
在这里插入图片描述
  默认的负载均衡策略是轮询,但如果你像修改策列,就需要在Nginx.conf配置文件中进行配置,例如如果你要用最少连接策略,则配置加一个东西:
在这里插入图片描述
  如果是用权重:
在这里插入图片描述

  Nginx也有故障排查的功能,也就是发送心跳包的形式,需要设置两个参数,max_fails和fail_timeout,第一个参数是最大失败次数,第二个参数代表单次连接超时时间,如果我Nginx发送给某一个服务器连续3次超时,则有问题,剔除该服务器。
在这里插入图片描述

设置Nginx集群session共享

  Nginx环境下存在session不同步的问题,例如客户端发送登陆请求后给nginx,nginx再分配给不同的服务器处理这个登陆,登陆成功后,登陆的账号密码存储在该服务器的session中,例如下面的session-user:laoqi,此时用户按F5刷新一下,他又把请求给了第二台服务器,这样就会回到登陆页。
在这里插入图片描述
  要解决这个问题,就需要用到redis,之前问题出现的原因是session信息放到自己的主机上,现在我们把这些登陆信息放到redis服务器里面,多台后端服务器共享一个redis资源,这样拿登陆的数据就去redis服务器里面拿就不会出现这种情况了。
在这里插入图片描述

  要使用redis服务器,spring提供了一个子工程叫做spring session,spring session提供了管理用户session的API实现,把spring session注入容器后,进行redis的配置,也就是redis配置在哪台主机上,端口号是多少,密码是多少等等。
  配置好后,他会自动的对session的操作进行监听。Session放在redis里有一个过期时间,当session放入redis后就需要设置redis里对session的过期时间。
在这里插入图片描述

Nginx静态资源缓存

  Tomcat中的静态资源包括css、js,这些是不怎么变的静态资源,每次访问都会对他们进行解析,当访问量大的时候每次都要解析就很消耗资源。所以需要用到Nginx的静态资源缓存来降低Tomcat的压力。
在这里插入图片描述
  要使用Nginx静态资源缓存,先要在Nginx.conf中配置两个文件夹,一个是临时文件夹,一个是设置缓存目录。然后配置哪些静态资源需要进行缓存,一般包括gif、jpg、css、png、js等。配置好后,就可以和之前讲的把对象进行缓存结合起来,process方法后存储的对象放在静态资源缓存的地方,就可以了。

Nginx资源压缩

  数据传输需要用到带宽,而带宽的租赁特别贵,所以带宽大小就有限制,因此我们需要把我们的数据进行压缩。
在这里插入图片描述
  当秒杀开始的时候,如上在100mbps的带宽情况下,一下只能让128位用户下载我们的js。所以,我们用Nginx把缓存的js这些压缩传送给浏览器自己解压。也需要在nginx.conf中配置即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章