秒杀项目总结

秒杀就是同一个时刻有大量的请求争抢购买同一个商品,并且完成交易的过程

也就是大量的并发读和并发写

先制作一个增删改查的秒杀系统,但是想让这个系统支持高并发访问就没那么容易了,

如何让这个秒杀系统面对百万级的请求流量不出故障,如何保证高并发情况下的数据一致性问题

rabbitmq主要是用来做一些异步,解耦模块,也可以用来流量削峰

课程介绍:

先是搭建项目

然后是分布式共享session

然后是开发秒杀功能(简单的增删改查),进行压力测试(可能会出现商品超卖,并发扛不住的问题):这里主要是四个功能,商品列表,商品详情,秒杀,订单详情

最后进行优化(主要是三方面的优化,页面优化,服务优化,接口安全上的优化)

页面优化:主要是缓存(页面缓存,url缓存,对象缓存),还有静态化分离(页面静态化,前后端分离),还有静态资源的优化

服务优化:RabbitMQ消息队列,分布式锁

安全优化:隐藏秒杀的地址(前期这个地址不会出来,只有到点了,才会出现秒杀的地址,让用户进行秒杀),验证码(区分是真实的用户还是脚本),接口限流(对于频繁访问的ip做一些控制)

总共五张表,用户表,商品表,秒杀商品表,订单表,秒杀订单表

由于商品可能秒杀和不秒杀价格可能同时存在,所以特地建一张秒杀商品表,区分于商品表

1.数据库

用户表t_user

(1)id  用户id  也就是手机号码

(2)nickname  昵称

(3)password  MD5(MD5(password+salt)+salt)  经过两次md5加密

(4)salt

(5)head  头像

(6)register_date  注册时间

(7)last_login_date 最后一次登陆时间

(8)login_count    登录次数

商品表t_goods

(1)id  商品id

(2) goods_name

(3)goods_title

(4)goods_img

(5)goods_detail  

(6)goods_price

(7)goods_stock  商品库存 ,-1表示数量没有限制

订单表t_order

(1)id  商品id

(2) user_id   用户id,谁创建的这个订单

(3)goods_id  商品id

(4)delivery_addr_id   收货地址id

(5)goods_name  商品名称

(6)goods_count  商品数量,买几个

(7)goods_price  商品单价

(8)order_channel  下单渠道,1表示pc端,2表示安卓,3表示ios

(9)status  订单状态:0表示新建未支付,1表示已支付,2表示已发货,3表示已收货,4表示已退款,5表示已完成

(10)create_date   订单创建时间

(11)pay_date   支付时间

t_seckill_goods  秒杀商品表

(1)id  秒杀商品id

(2)goods_id  商品id

(3)seckill_price  秒杀价格

(4)stock_count  库存数量

(5)start_date  秒杀开始时间

(6)end_date   秒杀结束时间

t_seckill_order 秒杀订单表

(1)id  秒杀订单id

(2)user_id   用户id

(3)order_id   订单id

(4)goods_id   商品id 

秒杀商品表    有人说这个表是多余的,直接在商品表中增加一个字段,这个字段为0表示普通商品,这个字段为1,表示秒杀商品,但是这样有一些问题,商品的秒杀时和平常的价格是不一样的

1. 登录功能

秒杀项目总结_第1张图片

登陆成功就会跳转到商品列表页面list.html,点击商品,进入详情页detail.html

而且登陆成功需要生成一个登录凭证ticket,将登录凭证保存到cookie里面

//生成一个随机字符串作为用户的ticket
String  ticket=UUIDUtil.uuid();

//user是用户实体类对象,将ticket字符串:user对象添加到session里面
request.getSession().setAttribute(ticket,user);


//再将session添加到cookie里面,第三个参数是这个cokkie字段的名字
CookieUtil.setCookie(request,response,"userTicket",ticket);

 登录后我们查看cookie:

 2.分布式session

如果我们将登录代码部署在多台服务器上面,

ngnix采用的默认的负载均衡策略是轮询,请求会按照时间顺序逐一分发到后端服务器上去

也就是说:一开始我们可能是在tomcat1上面去登录,这样用户信息就存在tomcat1的session里面,接下来请求可能就被分发到tomcat2上面,此时tomcat2的session里面没有用户信息,于是又要重新登录,这就是分布式session问题

有以下几个解决问题的方法:

(1)Session复制

就是将其中一台服务器的session复制给其他服务器

这样做的优点在于不需要修改代码,只需修改tomcat的配置即可,非常简单

缺点是session的同步传输会占用内网的带宽,多台tomcat同步的话性能会指数级下降,每一台服务器都保存相同的session特别占用内存,造成了内存浪费(冗余)

(2)前端存储 

这样的优点是不占用服务器端内存

但是缺点是存在安全风险,cookie很容易被拦截,而且大小也受制于cookie的大小

(3)Session粘滞

当用户发出第一个request后,负载均衡器动态的把该用户分配到某个节点,并记录该节点的路由,以后该用户的所有request都绑定到这个路由,该用户只会与该server发生交互

缺点是:如果某台服务器挂掉了,Session就会丢失

(4)后端集中存储(也叫使用redis实现分布式session)

就是将这个session存储在redis里,请求来了之后,不管哪个服务器分配来处理这个请求,去redis里面查出这个session

我们采取的是后端集中存储的方法,将session存储到redis里面,不管哪个服务器要用户信息,直接去redis中获取

使用redis实现分布式session,有两种实现方法

方法一:使用spring session来实现

引入redis和spring session的依赖

秒杀项目总结_第2张图片

在application.yml配置文件中进行配置

redis:
  host:192.168.10.100   //redis服务器的ip地址
  port:6379
  database:0  //默认操作几号数据库
  ......
     

秒杀项目总结_第3张图片

 方式二:

@Autowired 
private  RedisTemplate   redisTemplate;

//生成一个随机字符串作为用户的ticket
String  ticket=UUIDUtil.uuid();

//存入redis,user表示用户实体类对象
redisTemplate.opsForValue.set("user"+ticket,user);


//将ticket放入到cookie里面
CookieUtil.setCookie(request,response,"userTicket",ticket);
User  user=redisTemplate.opsForValue().get("user:"+userTicket);
if(user==null)
{
   跳转到登录页面
}
else
{
   跳转到商品列表页面
}

秒杀项目总结_第4张图片

注意:这里虽然使用了redis,但是这里我们用的redis只是装一个根节点,哨兵,主从复制,读写分离,集群都没有考虑

redis的高级数据结构位图bitmaps,HyperLogLog,GEO没有涉及

磁盘持久化方案也没有涉及

3.拦截器判断用户有没有登录

之前每次都要从session或者redis里面根据ticket字符串来获取user对象,然后去数据库中查询这个用户是否存在,

秒杀项目总结_第5张图片

具体参考这篇博客: 拦截器Interceptor_Pr Young的博客-CSDN博客

4.秒杀功能

用户登录成功后,跳转到商品列表页,商品列表页有一个详情按钮,点击详情跳转到商品详情页,在这个页面可以进行秒杀(在秒杀还没开始的时候,秒杀按钮是灰色的,不能按的,倒计时结束,这个秒杀按钮才可以按),秒杀成功后就会进入订单页

这个功能里需要创建四张数据表   商品表,秒杀商品表,订单表,秒杀订单表

还需要开发  商品列表页,商品详情页,订单页三个页面

商品列表页:

秒杀项目总结_第6张图片

 商品详情页

秒杀项目总结_第7张图片

 怎么实现倒计时功能呢?

设置一个秒杀开始时间t1和秒杀结束时间t2

当前的时间在t1之前就,秒杀还未开始

当前的时间在t1之后,在t2之前,就开始秒杀

当前时间在t2之后,就结束秒杀

点击秒杀按钮的时候就会调用秒杀方法,先查此时数据库中该商品是否有库存(去秒杀商品表中查找),并且判断当前用户是否已经秒杀过(去秒杀订单表中查找,如果表中已经存在这个用户id表示这个用户已经秒杀过了,不能再秒杀了)

5.使用JMeter进行压测

当100个人同时去点击这个立即秒杀

压力测试:测的是并发,

QPS:Queries Per Second,每秒的查询率,是一台查询服务器每秒能够响应的查询次数(每秒执行查询sql的次数)

TPS:Transactions Per Second,意思是每秒事务数,从客户机发送请求开始计时,到服务机收到请求然后发送给客户机处理结果,客户机收到处理结果停止计时

JMeter:选择某一个页面进行压力测试

1000个线程 访问这个页面,吞吐量为262

秒杀项目总结_第8张图片

测试商品列表接口和秒杀接口(商品列表接口只需要读取数据,而秒杀接口需要更新数据)

商品列表页windows优化前qps:1332,Linux优化前qps  207

秒杀接口:5000个用户同时秒杀id=1的商品

window优化前qps:785   linux优化前qps:

qps小还不是问题,问题在于发现库存变为负数(也就是超卖了)

而且还有个问题就是:秒杀接口没有隐藏起来(虽然按钮是灰色的,但是你知道秒杀路径的话依然可以直接访问这个路径)

6.第一个优化  使用缓存

(1)页面缓存:使用的是thymleaf模板,需要从服务器端查询数据,然后将数据全部放到浏览器做展示示,可以将这些数据放到redis里面

缓存商品列表页

秒杀项目总结_第9张图片

//redis中获取页面,如果可以获取到页面,直接返回页面
String html=redisTemplate.opsForValue().get("goodsList");
if(StringUtil.isEmpty(html)==false)
{
     return   html;
}
else//页面为空就要将其存入缓存
{
   先去获取到html,然后存到redis里面
   redisTemplate.opsForValue().set("goodsList",html);
   return  html;
}

(2)url缓存:把商品详情页也缓存起来,还要传入一个商品ID

String html=redisTemplate.opsForValue().get("goodsList"+goodsId);

尽管做了缓存,还是需要从redis中发送整个页面给前端,于是后面还会进行前后端分离

(3)对象缓存:

吞吐量变成了2394(之前是1332)

7.第二个优化:“页面静态化:即使使用页面缓存,还是需要将一整个thymleaf引擎发送给前端,前后端分离,前端就是html,里面的一些动态数据才需要从后端发给前端

静态化商品详情页面

以上两个优化都是页面优化,接下来是接口优化

8.接口优化

即使加了缓存之后,有些接口也要访问数据库,比如去数据库中获取某件商品的库存,然后扣减库存,这部分就要通过redis来扣减库存

即使你用了redis来扣减库存不需要去数据库中读取数据了,但是我们仍然需要频繁的和redis进行交互,redis放在一个单独的服务器上,所以我们还是需要频繁的和redis服务器进行交互,可以通过内存标记来减少对redis服务器的访问

下单操作也要进行优化,下单的时候如果直接去找数据库,数据库仍然是扛不住这么大量的并发,可以用队列,先让请求进入到队列里面进行缓冲,通过队列进行异步下单增强用户体验

总结:(1)通过redis预减库存,减少对数据库的访问

      (2)内存标记,减少对redis的访问

    (3) 请求进入队列缓存,异步下单

redis预减库存,如果库存不足,直接返回库存不足,这样已经可以大幅提高性能

如果库存是足的,就将这个请求封装成一个对象,发送给rabbitmq,这样前期大量的请求过来依然可以快速处理掉,后面消息队列再慢慢处理(起到一个流量削峰的作用),此时显示排队中,

也就是将redis中每个秒杀商品:这个秒杀商品的库存存入到redis里面

使用topic模式

@Configuration
public  class   RabbitMQTopicConfig
{
    @Bean
    public  Queue    queue()
    {
        return   new   Queue("seckillQueue");//队列名称就叫queue,消息持久化
 
    }

    //定义交换机
    pubcli  TopicExchange  topicExchange()
    {
       return  new  TopicExchange("topicExchange");
    }

 
    @Bean  //绑定队列到交换机,需要带上路由键route key
    public    Binding  binding()
    {
       return   BindingBuilder.bind(seckillQueue()).to(topicExchange()).with("*.queue.#");
    }
}

发送方:

 接收方:

@RabbitListen(queues="seckillQueue")
//接收到消息开始下单
public   void    receive(String message)
{
        //将字符串转为对象
        SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
        
        //获取要秒杀的商品的id以及用户对象
        Long goodsId = seckillMessage.getGoodsId();
        User user = seckillMessage.getTUser();

        //根据商品id获取商品对象
        GoodsVo goodsVo = itGoodsServicel.findGoodsVobyGoodsId(goodsId);

        if (goodsVo.getStockCount() < 1) 
        {
            return;
        }

        //判断是否重复抢购,查询redis中是否已经有该用户id:该秒杀商品id这一行数据
        TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (tSeckillOrder != null) 
        {
            return;
        }
        
        //正式下单
        itOrderService.secKill(user, goodsVo);
}
//将请求封装成一个对象
 SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);


//发送这个对象到消息队列中
mqSender.send(seckillMessag);

9.最后是接口安全性保障

也就是黄牛把脚本准备好,快速抢秒杀商品,导致很多正真的用户抢不到秒杀商品

隐藏接口地址

用验证码隔离脚本和延长请求时间长度

接口限:漏桶算法(也可以采用令牌桶算法)

你可能感兴趣的:(springboot,springboot)