美文网首页
优化使用springboot完成的秒杀案例

优化使用springboot完成的秒杀案例

作者: coder爱唱歌 | 来源:发表于2020-02-24 12:48 被阅读0次

    之前学习了齐毅老师的秒杀优化课程,最近在回顾技术栈的时候,觉得非常有用,所以花些时间总结一下。这篇文章很长,但我感觉都是实实在在能提升程序性能的技术总结,需要结合github上的代码来看文章,代码根据每个优化的点都做了不同的分支,方便查看。

    branch.png

    增删改查每个人都会做,但是秒杀系统,仅仅增删改查肯定是远远不够的,因为,要考虑到瞬时高并发,主流电商的秒杀qps峰值都在100w+,如果全部靠查询数据库,页面一定卡死,数据库崩溃。而且,仅仅靠数据库,在高并发必然会导致商品超卖。系统崩溃和商品超卖都是不能接受的问题,所以需要逐步优化。

    首先,拉取代码,并切换分支,搭建初步增删改查的项目

    git clone git@github.com:along05/secKill.git;
    git checkout step1:ssm增删改查版本
    

    执行数据库脚本,启动项目并访问

    http://localhost:8080/good?gid=2000
    

    项目是一个最基本的商品页面展示,点击抢购会往数据库添加一条记录的demo程序。然后接下来一步一步进行优化。

    优化的点

    • 稳定不太变化的数据,减少数据查询
    • redis解决超卖
    • rabbitMQ削峰和限流
    • nginx负载均衡
    • 解决负载均衡多个api之间session不同步的问题
    • nginx静态资源缓存
    • nginx资源压缩
    • cdn内容分发
    • 流量防刷以及反爬虫

    1.稳定数据优化方式

    问题分析:电商和我们平时app中,90% 以上都是数据读取操作,在海量数下,数据库最后可能成为高并发的瓶颈,因此减少数据库交互是首先需要考虑的事情。但是要区分静态数据和动态数据。长期不变的数据,比如商品详情,图片,介绍等适合做缓存,但是评论就不适合做缓存。

    解决方案1:reids缓存

    查询一次数据库后,把商品详情,图片,介绍等放入redis,然后下次redis有的话直接从reids查询,避免再次查数据库。
    具体切换到 step2-静态数据优化 分支

    git checkout step2-静态数据优化
    

    安装redis

    docker pull redis;
    docker run -d --name redis1 -p 6379:6379 redis --requirepass "123456"
    

    安装完redis,对应在yml中修改自己的redis配置,访问下面地址,查看redis,就会发现redis中缓存了商品相关信息,具体实现,查看代码,很简单。

    http://localhost:8080/good?gid=2000
    

    解决方案2:html页面静态化

    页面静态化是指把动态页面,jsp,freemarker等为每一个商品生成一个html静态页面。动态数据例如评论就在html页面用ajax异步获取。这样,就直接访问html避免了页面数据查询,解析的过程。

    逻辑流程
    • 首先把所有商品生成固定html,然后编写定时器,把每5分钟更新过的的商品重新查询出来,重新生成固定的html页面
    • 对于例如评论等动态数据,用ajax动态获取
    • 因为是静态页面,需要使用nginx做页面转发
    操作流程
    • 切换到 step2-静态数据优化 分支,启动项目
    • 执行http://localhost:8080/doStatic,修改doStatic方法中放静态文件的目录。并把static中layui的包拷到生成的html目录,要引入。goods.ftl中路径写的./
    • 启动nginx,脚本如下。
    • 访问http://localhost/page/2000.html 就能出现页面。

    总结:http://localhost/page/2000.html这里的流程
    是首先去容器内部/usr/share/nginx/html/去找html文件,然后其实是访问到了挂载在本地生成html的目录,然后html里面发起ajax请求去获取评论,实际是通过nginx把请求通过upstream转发到本地。

    docker安装nginx,指令如下:
    --- nginx
    docker pull nginx;
    docker run -d --name nginx -p 80:80 -v /Users/along/websoft/nginx:/etc/nginx/conf.d -v /Users/along/Desktop/template:/usr/share/nginx/html nginx
    
    上面启动脚本意思是:映射80端口,然后挂载配置文件目录和放html的目录到nginx里面
    启动脚本和html位置在要和上面启动命令中的一致。
    
    --- nginx 脚本
    #因为容器内部需要访问到外部的接口,所以需要在容器内容用ip+端口访问本地接口
    upstream seckill {
        server 192.168.5.3:8080;    
    }
    server {
        listen       80;
        server_name localhost;
        #把接口转发到本地起的项目,我的是192.168.5.3:8080;  
        location / {
        proxy_pass http://seckill;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        #页面转发地址是容器内部地址  
        location /page {
           index index.html;
           alias /usr/share/nginx/html/;
        }
    }
    监听80端口,访问/放转发到本地接口,访问page的时候,定位到nginx的html放置目录,然后之前挂载过这个目录,就能访问到本机的挂载目录。
    

    两种方案总结:

    • 方案一把静态数据存入到redis,操作简单。但是还是需要付出解析动态页面的时间。
    • 方案二非常复杂,需要生成所有html,耗费空间,但是理论上,直接加载html是最快的。

    2.redis解决秒杀超卖

    现象分析

    使用redis之前的一般做法是,查询数据库,如果剩余大于0,就抢到商品,并把剩余数量减一,并更新数据库。

    int num = goodDao.selectRate(num) ; 
    if(num>0){
        //处理生成订单逻辑
        num = num -1; 
        //更新数据
    }
    

    但是很明显,查询数量和更新数量之前是有时差的,有的请求执行到了生成订单逻辑,东西抢到了,但是没来得及更新数据库,但是有的请求在这时获取了剩余数量,那么就会超卖。
    选取redis因为它是单线程模型,内存存储,官网宣称支持10w qps。而且天生分布式支持。

    解决方案:

    逻辑流程

    • 每隔5秒定时器扫秒杀表t_promotion_seckill
    • 如果扫到开始秒杀,就构建一个商品的list队列,key为seckill:count:秒杀id ,有几个商品,就往list里面初始化几个数据。
    • 当用户点击了秒杀按钮就left pop出商品,并把抢到商品的用户存到一个redis set中。弹出一个商品,就存一个userId.

    操作流程:

    • 切换代码分支到 step3-redis解决超卖
    • 相关新增代码在定时器和GoodController的doOrder方法。
    • 定时器会扫到 t_promotion_seckill , 如果扫不到,请查看sql,修改数据的状态为0

    3.rabbitMQ削峰和限流

    现象分析

    在抢购以后,往往会发起存数据库,支付等操作,但是很明显,之前的从redis中取商品,并添加中奖人到redis这个操作是很快的,但是订单入库,支付等操作是很慢的。假设处理取商品,存中奖人每秒可以处理1000,但是处理订单,支付等一秒处理100个,那么,如果是一连串操作,很明显,后面瓶颈,造成前面操作等待。严重的情况,由于支付过慢崩溃,导致前面一系列流程全部崩溃。
    总而言之就是前台业务处理能力和后台业务能力不对称所导致的

    解决方案 - 使用rabbitMQ消息队列

    因为可以分离业务,前台业务处理快,尽管处理,然后后面生成订单,支付等在mq中按自己能处理的量异步处理。
    rabbitMQ是使用Erlang开发,也有自己的虚拟机,编译后到处运行,而且编写分布式应用简单。

    逻辑流程

    • 当redis取出商品,添加中奖id后,往rabbitMQ队列发送一条消息,这里就是消息生成者
    • 然后在消息消费者那里执行订单入库等逻辑

    操作流程

    • 切换分支到
    • docker启动一个rabbitMQ服务,并在rabbitMQ管理界面创建一个exchange-order的交换机,并且创建一个queue-order的队列,把他们绑定
    • 在yml编写rabbitMQ相关配置
    • 在PromotionSecKillService 编写消息生产者
    • 在service编写OrderConsumer消费者逻辑

    docker起rabbitMQ

    docker pull rabbitmq;
    docker run -d -p 15672:15672  -p  5672:5672  
    -e RABBITMQ_DEFAULT_USER=admin 
    -e RABBITMQ_DEFAULT_PASS=admin 
    --name rabbitmq 
    rabbitmq
    

    用admin和admin打开loaclhost:5672.

    4.nginx负载均衡

    LoadBalance:负载均衡,把任务分配到多个服务器上进行处理,进而提高效率,缩短执行时间。
    

    当一台机器性能到达极限的情况下,横向拓展机器的架构是非常好的,能在不改动代码的情况下成倍的提升效率。

    逻辑流程:

    • nginx里面配置各个服务地址,请求同一个api的时候,按一定的策略分发请求到不同服务。
    • 在docker启动nginx

    操作流程

    • 切换代码到 step5-nginx负载均衡 分支,其中新增了一个 LBController 测试负载均衡的控制器。
    • 然后在启动设置里面设置 -Dserver.port=8081,-Dserver.port=8082,-Dserver.port=8083三个配置,启动三个服务。
    • 然后启动nginx
    • 调用 localhost/LB 这个地址,会发现,请求到不同的端口。

    nginx的分发策略

    • Round-Robin,轮询调度算法,当配置未指明的时候,默认就是轮询算法。就想听歌列表循环。
    • least-connected,下一次链接将被链接到活跃链接最少的机器上
    • ip-hash,利用IP地址哈希进行分配 。固定ip的请求分发到固定ip的服务
    • weight-balancing,按照权重分配,也就是权重越大,分到的请求越大。能力越大,责任越大。

    我比较喜欢使用权重策略,基本都是根据服务器性能来分配。

    nginx的配置以及启动脚本。

    #nginx.conf,配置中的server是本地ip,使用权重的策略
    upstream lb {
        #least_conn ; 最少连接
        #ip hash ; ip策略
        #权重
        server 192.168.5.3:8081 weight=4;
        server 192.168.5.3:8082 weight=1;
        server 192.168.5.3:8083 weight=2;
    }
    location / {
        proxy_pass http://lb;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    nginx在docker中的启动脚本和上面相同。
    

    5.解决负载均衡多个服务之间session不同的问题

    问题分析

    在部署的多个服务中如果使用session传递数据,可能存在获取不到的情况,因为假设在8081中执行登陆,session只会存在8081这个服务中,但是下次请求分发到8082或者8083就获取不到session了。

    解决方式

    • 利用redis存储session的内容。
    • org.springframework.session 这个jar包可以解决问题。只需要引入这个包,并在启动类加上 @EnableRedisHttpSession 注解,无需修改任何代码。

    操作流程。

    • 切换代码到 step6-解决各个服务间session不同步问题 分支
    • 和上一步负载均衡一样启动三个服务,开启nginx
    • 首先把启动类上的 @EnableRedisHttpSession 注释掉,然后先请求 localhost/login?u=along,然后在多次请求localhost/check发现,有时候能获取到session,有时不能获取到。
    • 然后把注解打开,再执行上面的操作,发现,每次都能获取到session,查看redis,发现,redis多了session的信息。

    6.nginx静态资源缓存

    现象分析

    像js,css,图片,字体等静态资源,他们都是一个个的url,每次访问的tamcat都会进行url解析和绑定。但是上线以后,它们几乎是不会修改的。所以在高并发的情况下很浪费资源。

    处理方案

    nginx区分静态资源和接口url,把url接口送往后端服务器处理,静态资源在nginx端缓存处理。
    主要是修改nginx的配置,配置如下

    #在nginx容器内的/home下面定义缓存文件夹。并设定2层目录,缓存大小以及过期时间。
    proxy_temp_path /home/nginx-temp;
    proxy_cache_path /home/nginx-cache levels=1:2 keys_zone=lb-cache:100m inactive=7d max_size=20g;
    
    upstream lb {
        #least_conn ; 最少连接
        #ip hash ; ip策略
        #权重
        server 192.168.5.3:8081 weight=4;
        server 192.168.5.3:8082 weight=1;
        server 192.168.5.3:8083 weight=2;
    }
    #用正则匹配静态资源,~*是不区分大小写。
    location ~* \.(gif|jpg|css|png|js|woff|html)(.*){
           proxy_pass http://lb;
           proxy_set_header Host $host;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           #使用上面定义的缓存地址
           proxy_cache lb-cache;
           proxy_cache_valid 200 302 24h;
           proxy_cache_valid 301 5d;
           proxy_cache_valid any 5m;
           expires 90d;
        }
    location / {
        proxy_pass http://lb;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    

    然后重启nginx就好了,访问以后,进入容器

    docker exec -it nginx /bin/bash 
    cd /home/nginx-cache 
    如果上面的文件夹出现了两层目录的静态文件,说明设置成功。
    

    7.nginx资源压缩

    现象分析

    当静态资源被压缩后,降低了后端tomcat的压力,但是在实际开发过程中,大量的压力不在于处理速度,而是在于带宽。带宽决定了下载的总速度,在总速度不变的情况下,资源传到浏览器的大小约小越好。越快下载完资源,卡顿的越少。

    解释一下带宽和下载速度的关系把

    带宽是网络中某一点到另一点所能通过的"最高数据率",也就是瞬时最高的下载速度。
    关于带宽和下载速度:首先了解一下存储的单位换算

    • 1GB = 1024MB
    • 1MB = 1024KB
    • 1KB = 1024B
    • 1B = 8bits
      网络传输是以bits为单位,以1M带宽为例,也就是1M bits,也就是瞬时下载速度做多为:
    1M = 10^6;
    bits换成B除以8,B换成kb除以1024
    1M bits/s = 1000000bits/s = 125000 B/s ≈ 128kb/s 
    

    解决方案

    nginx将静态资源打包成gzip传给浏览器,然后浏览器解压缩gizp(这一步浏览器自动完成)获取到资源文件。最少能较少30%内存,最多减少80%的内存损耗。

    nginx配置

    #打开gzip配置
    gzip on;
    gzip_min_length 1k;
    gzip_types text/plain application/javascript text/css application/x-javascript font/woff;
    #禁止IE 1-6 
    gzip_disable "MSIE [1-6]\.";
    gzip_buffers 32 4k;
    #压缩级别1-9,选1就行了,大小差别不大
    gzip_comp_level 1;
    
    #在nginx容器内的/home下面定义缓存文件夹。并设定2层目录,缓存大小以及过期时间。
    proxy_temp_path /home/nginx-temp;
    proxy_cache_path /home/nginx-cache levels=1:2 keys_zone=lb-cache:100m inactive=7d max_size=20g;
    
    upstream lb {
        #least_conn ; 最少连接
        #ip hash ; ip策略
        #权重
        server 192.168.5.3:8081 weight=4;
        server 192.168.5.3:8082 weight=1;
        server 192.168.5.3:8083 weight=2;
    }
    #用正则匹配静态资源,~*是不区分大小写。
    location ~* \.(gif|jpg|css|png|js|woff|html)(.*){
           proxy_pass http://lb;
           proxy_set_header Host $host;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           #使用上面定义的缓存地址
           proxy_cache lb-cache;
           proxy_cache_valid 200 302 24h;
           proxy_cache_valid 301 5d;
           proxy_cache_valid any 5m;
           expires 90d;
        }
    location / {
        proxy_pass http://lb;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    

    在docker重启nginx。访问页面,当看到浏览器,检查看到css,js等的response header中,编码为gzip。就说明设置成功了。

    8.cdn内容分发

    现象分析

    对静态文件完成了gzip压缩,但是对图片等已经被压缩过的资源gzip传输效果并不好,所以对图片的带宽消问题并没有解决。

    解决方案

    图片可以考虑使用第三方cdn解决带宽问题。

    cdn简介

    cdn全称Content dekivery network,就是内容分发网络。
    例如中国很大,北京和深圳就隔者2000多公里,如果北京访问深圳的网络,必然延时很高。cnd就是把内容放在一台中心服务器上,然后各地有很多节点服务器,中心服务器会把内容同步到各地的节点服务器,然后就进访问资源。

    优点

    • 就近访问,降低网络延时
    • 把对带宽的压力转移到了第三方
    • 数据多备份,一个服务挂了可以切换到另一个,就就是高可用。

    使用

    可以买阿里云,腾讯云,七牛的cdn服务。
    主要流程是买服务,下载客户端,把图片等资源上传到客户端,自动同步,然后为oss配置cdn,进行域名解析。就能通过域名访问到图片,最后就去修改项目中静态资源的路径。

    9.流量防刷以及反爬虫

    流量防刷

    有些人会恶意一直访问网站,甚至用脚本去刷页面。
    所以在一定时间内只允许一个用户访问固定的次数,一旦超过了就不允许访问。

    爬虫

    会有人用爬虫,爬网站,利用超链接分析内容,提取网站的重要信息,不仅拷贝走了数据,而且,还非常消耗服务器资源。

    解决方案

    • 在代码中,编写拦截器
    • 对每个用户,记录ip对应的访问次数,在redis中用ip+请求头信息作为key,存1分钟内访问次数,每次访问count++
    • 如果超过设置的访问次数,就不再允许访问。
    • 每一分钟清空一次key

    操作流程

    • 切换代码到step7-流量防刷与反爬虫分支
    • 启动nginx和三个端口分别启动项目
    • 访问localhost/lb,并多次刷新
    • 观察redis是否会有key,以及是否会返回禁用的信息

    总结

    终于写完啦。这篇文章主要是对代码和网络等几个层面做了优化,主要使用了nginx,redis,rabbitMQ等热门工具,希望对各位有帮助,如果有问题,欢迎指正,一起讨论。

    相关文章

      网友评论

          本文标题:优化使用springboot完成的秒杀案例

          本文链接:https://www.haomeiwen.com/subject/esomqhtx.html