秒杀项目总结

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

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

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

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

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. 登录功能

登陆成功就会跳转到商品列表页面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的依赖

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

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

 方式二:

@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
{
   跳转到商品列表页面
}

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

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

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

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

​​​​​​去cookie面查询ticket里面是否为空,不为空拿着这个value值去redis里面查询user对象,然后查询数据库中是否存在这个user对象

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

4.秒杀功能

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

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

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

商品列表页:

 商品详情页

 怎么实现倒计时功能呢?

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

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

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

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

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

5.使用JMeter进行压测

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

压力测试:测的是并发,

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

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

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

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

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

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

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

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

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

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

6.第一个优化  使用缓存

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

缓存商品列表页

//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.最后是接口安全性保障

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

隐藏接口地址

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

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

登录后,进入商品列表页,点击商品图片,进入商品详情页,部分商品的商品详情页有秒杀按钮,当到了秒杀时间的时候,点击秒杀按钮,然后进行秒杀,秒杀成功就返回订单详情页,秒杀失败返回秒杀失败

拦截器拦截用户在秒杀的时候是否登录了,没有登录的用户是无法进行秒杀的

并且采用lua脚本解决了超卖的问题,超卖的原因是查询秒杀商品的库存和减少和秒杀商品的库存两个操作不是原子性的,将这两个操作添加到lua脚本里面就可以保证这两个操作的原子性

decrement

并且进行了优化,

(1)redis缓存商品库存+rabbitmq异步下单

用redis来缓存秒杀商品的库存,因为大量的请求过来,其实只有很少的一部分能够秒杀成功,大量的请求都是秒杀失败的,所以这些秒杀失败的请求其实不用去访问数据库,先去查询redis中的库存是否为0,如果为0就直接返回秒杀失败,如果不为0,将下单操作发送到rabbitmq消息队列中去,等rabbitmq消息队列处理完这条消息之后再发送给用户订单页面

(其实就是将用户的id和秒杀商品的id封装成一个对象,发送到消息队列中,消息队列来进行一条消息一条消息的处理)

qps从....提高到了多少

此外还进行了接口安全的优化:

(1)隐藏秒杀接口的地址

当点击获取秒杀的时候,不是开始秒杀,而是获取了一个真正的秒杀地址

(2)添加验证码

输入验证码才能进行秒杀

(3)接口限流  限定同一个用户1s内只能点击5次 如何保证用户1s内只能点击5次

令牌桶算法:

其他可能会问的问题

(1)如何确保用户不重复下单:

redis中存储了key是用户id+秒杀商品的id,value是订单号,用户请求来了之后,先是将用户的id+想要秒杀的商品的id作为key去查询是否存在这样的一条key,value键值对,存在再去判断秒杀商品的库存是否为0,进行下一步判断

(2)如何保证redis和数据库的一致性

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值