秒杀抢购设计

一、秒杀设计细节

  秒杀系统的几个细节:瞬间高并发、页面静态化、秒杀按钮、读多写少、缓存问题、库存问题、分布式锁、MQ异步处理、限流。

    1、瞬间高并发

      一般在秒杀时间点前几分钟,用户并发量才真正突增,达到秒杀时间点时,并发量会达到顶峰。

      一瞬间秒杀就会结束,之后用户并发量又会急剧下降,所以这个峰值持续的时间其实是非常短的,即瞬时高并发的情况。

      对于瞬时高并发的场景,可以从以下几个方面入手:

      1)页面静态化

      2)CDN加速

      3)缓存

      4)MQ异步处理

      5)限流

            

    2、页面静态化

      活动页面是用户流量的第一入口,所以是并发量最大的地方

      如果这些流量都能直接访问服务端(即动态秒杀页面),恐怕服务端会因为承受不住那么大的压力,而直接挂掉      

      活动页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,通常情况下,会对活动页面做静态化处理。用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。这样可以过滤掉大部分无效的请求。

      只做页面静态化其实还不够,还需要使用CDN(内容分发网络),使用户可以就近最快访问到活动页面,降低网络拥塞,提高用户访问响应速度和命中率。

 

 

    3、秒杀按钮

      秒杀开始前,秒杀按钮需要置灰不能点击,只有到了秒杀时间的那一时刻,秒杀按钮才会自动点亮,可以点击。

      如何在静态页面中,控制秒杀按钮,只在秒杀时间点时才点亮呢?答案是:使用js文件控制。

      为了性能考虑,一般会将css、js和图片等静态资源文件提前缓存到CDN上,让用户能够就近访问秒杀页面。

      那么,CDN上的js文件是如何更新的呢?

        秒杀开始之前,js标志为false,还有另外一个随机参数。

        当秒杀开始的时候系统会生成一个新的js文件,此时标志为true,并且随机参数生成一个新值,然后同步给CDN。由于有了这个随机参数,CDN不会缓存数据,每次都能从CDN中获取最新的js代码。

      此外,前端还可以加一个定时器,控制比如:10秒之内,只允许发起一次请求。如果用户点击了一次秒杀按钮,则在10秒之内置灰,不允许再次点击,等到过了时间限制,又允许重新点击该按钮

 

    4、读多写少

      在秒杀过程中,系统一般会先检查一下库存是否足够,如果足够才允许下单,写数据库。如果库存不足,直接返回该商品已经抢完。

      所以大量用户抢少量商品,每个用户都会查询库存,但抢到该商品的用户是极少数的。这是典型的读多写少的场景。

      如果库存数据保存在MySQL中,同时如果有数十万的请求进来同时去MySQL中查询库存是否足够,此时数据库可能会挂掉。因为数据库的资源连接十分有限,无法同时支持那么多的连接。

      所以秒杀的库存数据应该放在缓存中,如Redis。即使用了Redis,也需要部署多个节点。

  

    5、缓存问题

      通常情况下,我们需要在redis中保存商品信息,里面包含:商品id、商品名称、规格属性、库存等信息,同时数据库中也要有相关信息,毕竟缓存并不完全可靠。

      用户在点击秒杀按钮,请求秒杀接口的过程中,需要传入的商品id参数,然后服务端需要校验该商品是否合法。

      大致流程:用户选择商品秒杀,传入商品id,服务端根据商品id先从缓存中查询商品,如果商品存在,则参与秒杀。如果不存在,则需要从数据库中查询商品,如果存在,则将商品信息放入缓存,然后参与秒杀。如果数据库中商品不存在,则直接提示失败。

        问题1:缓存击穿,如果商品信息没有通过提前预热到缓存中,那么秒杀开始,大量请求同时访问数据库,造成缓存击穿的问题。或者商品信息放入了缓存并设置了生存时间,在缓存失效的那一刻,也会造成缓存击穿的问题。

          解决方案:1)通过分布式锁,获得锁的线程去访问数据库,并将数据放入缓存中

                 2)缓存预热,提前将商品信息预热到缓存中(推荐

        问题2:缓存穿透,如果有大量的请求传入的商品id,在缓存中和数据库中都不存在,这些请求不就每次都会穿透过缓存,而直接访问数据库了。

          解决方案:1)通过分布式锁,获得锁的线程去访问数据库,即使没有数据,也缓存空数据(生存时间设置短一些)

                 2)使用布隆过滤器,系统根据商品id,先从布隆过滤器中查询该id是否存在,如果存在则允许从缓存中查询数据,如果不存在,则直接返回失败(推荐

                

    6、库存问题

      秒杀商品的场景,在预扣库存之后,如果用户在规定的时间内没有完成支付,那么则需要关闭订单,回退库存

      另外还要注意库存不足和库存超卖的问题。

      1)使用数据库扣减库存

      update product set stock=stock-1 where id=product and stock > 0;

      问题:频繁访问数据库,我们都知道数据库连接是非常昂贵的资源。在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题

 

      2)使用Redis lua脚本扣减库存

      lua脚本能够保证查询库存和扣减库存的原子性,和Redis结合使用,能够完美解决库存超卖的问题。

      lua脚本如:

StringBuilder lua = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");    // 先判断商品id的库存缓存key是否存在,如果不存在则直接返回-1
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));"); // 获取到缓存的商品库存
        sb.append("    if (stock <= 0) then");
        sb.append("        return -1;");  // 如果库存不足,返回-1
        sb.append("    end;");
        sb.append("    if (stock > 0) then");
        sb.append("        redis.call('incrby', KEYS[1], -1);");
        sb.append("        return stock - 1;"); // 如果库存大于0,返回扣减后的库存
        sb.append("    end;");
        sb.append("end;");
        sb.append("return -1;"); // 商品库存key不存在,直接返回-1

 

    7、分布式锁

      在秒杀时,如果缓存中没有商品信息,那么将会从数据库中查询商品信息,并将查询到商品信息放入缓存中,然后返回。如果数据库中没有,则返回失败,空数据也放入缓存中。

      高并发情况下,要控制访问数据库的线程,否则大量请求同时访问数据库,会导致数据库扛不住压力而挂掉。因此只有获得分布式锁的线程才去查询数据库。

      分布式锁不再赘述,详见:https://www.cnblogs.com/yangyongjie/p/14145919.html

      那么获取不到分布式锁的线程,需要自旋等待获取锁的线程从数据库中查询数据并放到缓存中,然后从缓存中读取数据。

      可采用Thread.sleep(10);休眠,如10ms,再去缓存中查询数据,如果缓存中仍没有数据,则再次尝试获取分布式锁。直到成功为止。

 

 

    8、MQ异步处理

      在秒杀的场景中,有三个核心流程:秒杀-下单-支付

      在这三个核心流程中,真正并发量大的是秒杀功能;下单和支付功能的实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来

      特别是下单功能要做成MQ异步处理的。发送下单消息-MQ-消费消息进行下单

      而支付功能,比如支付宝支付,是业务场景本身保证的异步。

      对于异步MQ消息下单,需要注意以下几个问题:

      1)消息丢失

        秒杀成功,往MQ发送下单消息的时候,有可能会失败。如:网络问题。还有丢失的情况,如broker挂了,MQ磁盘问题等

        所以发送MQ时,即消息生产者要保证消息妥投。

        解决方案:加一张消息发送表

          秒杀成功后,在发送MQ消息之前,先将下单的消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。

          如果生产者把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了,造成了消息丢失。解决方案是:使用定时任务,增加重试机制,定时任务每隔一段时间去查询消息发送表中状态为待处理的数据,然后重新发送mq消息

      2)重复消费

        如果消费成功,由于网络问题,没有成功提交offset,或者rebalance,都会出现重复消费问题

        解决方案:加一张消息处理表

          消费者读到消息之后,先判断一下消息处理表,是否存在该消息,如果村子啊,表示是重复消费,则直接返回。如果不存在,则进行下单操作,接着将消息写入消息处理表中,再返回。(需要注意:下单和写消息处理表,放在一个事务中,保证原子操作)

        

 

    9、限流

       为了防止黄牛通过服务器直接访问秒杀接口,1秒钟可能会调用上千次,而手动抢购,1秒钟只能点击一两次。

      为了防止商品都被黄牛抢到,所以,我们有必要识别这些非法请求,做流量限制。目前有两种常用的限流方式:基于nginx限流和基于Redis限流。

      1)对同一用户限流

        为了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。限制同一个用户id,比如每分钟只能请求5次接口

      2)对同一ip限流

        有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了。

        限制同一个ip,比如每分钟只能请求5次接口。

        但这种限流方式可能会有误杀的情况,比如同一个公司或网吧的出口ip是相同的,如果里面有多个正常用户同时发起请求,有些用户可能会被限制住

       3)对接口限流

        别以为限制了用户和ip就万事大吉,有些高手甚至可以使用代理,每次都请求都换一个ip。这时可以限制请求的接口总次数

         在高并发场景下,这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口。看起来有点得不偿失

      4)加验证码

        相对于上面三种方式,加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况

        通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误

        现在各大互联网公司首选使用的移动滑块,虽然它生成速度慢,但比较安全

      5)提高业务门槛

        如,只有会员才能参与秒杀。

        秒杀前需要预约,限制预约的数量

 

 

二、茅台抢购设计

  1、架构设计

  

 

   2、预约流程

 

 

   3、抢购流程  

   使用lua脚本(不用分布式锁),库存查询和库存扣减原子化,避免高并发情况下不一致的问题。

   lua脚本示例如下:

StringBuilder lua = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");    // 先判断商品id的库存缓存key是否存在,如果不存在则直接返回-1
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));"); // 获取到缓存的商品库存
        sb.append("    if (stock <= 0) then");
        sb.append("        return -1;");  // 如果库存不足,返回-1
        sb.append("    end;");
        sb.append("    if (stock > 0) then");
        sb.append("        redis.call('incrby', KEYS[1], -1);");
        sb.append("        return stock - 1;"); // 如果库存大于0,返回扣减后的库存
        sb.append("    end;");
        sb.append("end;");
        sb.append("return -1;"); // 商品库存key不存在,直接返回-1

 

   库存扣减成功,发送MQ消息。

  流程梳理:

  1、点击抢购——2、进入结算页,点击提交订单——3、服务端校验和扣减库存——4、若抢购成功,发送MQ消息,消息MQ下订单——5、客户端查询到订单调用支付

 

   难点:

  1、接口防刷

    限流策略

  2、Redis集群稳定性

 

END.

posted @ 2021-08-23 16:07  杨岂  阅读(771)  评论(0编辑  收藏  举报