zoukankan      html  css  js  c++  java
  • 秒杀抢购设计

    一、秒杀设计细节

      秒杀系统的几个细节:瞬间高并发、页面静态化、秒杀按钮、读多写少、缓存问题、库存问题、分布式锁、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.

  • 相关阅读:
    2020杭电HDU-6863多校第八场Isomorphic Strings(Hash+学到的新东西)
    CodeForces 1395C-Boboniu and Bit Operations(位运算-暴力)
    洛谷P2585&ZJOI 2006-三色二叉树(树的染色-树形DP)
    洛谷P2016-战略游戏(树的最小点覆盖-树形DP)
    洛谷P2015-二叉苹果树(树形DP)
    洛谷P1352-没有上司的舞会(树形DP)
    CSUSTOJ 4004-你真的会吃零食吗?(打表)
    在线支付--支付宝
    使用rabbitmq rpc 模式
    在kubernetes 集群运行 odoo
  • 原文地址:https://www.cnblogs.com/yangyongjie/p/15176344.html
Copyright © 2011-2022 走看看