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

2020-02-24  本文已影响0人  coder爱唱歌

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

branch.png

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

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

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

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

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

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

优化的点

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避免了页面数据查询,解析的过程。

逻辑流程
操作流程

总结: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放置目录,然后之前挂载过这个目录,就能访问到本机的挂载目录。

两种方案总结:

2.redis解决秒杀超卖

现象分析

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

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

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

解决方案:

逻辑流程

操作流程:

3.rabbitMQ削峰和限流

现象分析

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

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

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

逻辑流程

操作流程

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的分发策略

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

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了。

解决方式

操作流程。

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的压力,但是在实际开发过程中,大量的压力不在于处理速度,而是在于带宽。带宽决定了下载的总速度,在总速度不变的情况下,资源传到浏览器的大小约小越好。越快下载完资源,卡顿的越少。

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

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

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.流量防刷以及反爬虫

流量防刷

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

爬虫

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

解决方案

操作流程

总结

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

上一篇下一篇

猜你喜欢

热点阅读