08 | 秒杀系统设计:你的系统可以应对万人抢购盛况吗?

你好,我是李智慧。

秒杀是电子商务应用常见的一种营销手段:将少量商品(常常只有一件)以极低的价格,在特定的时间点出售。比如,周日晚上8点整,开售1部1元钱的手机。 因为商品价格诱人,而且数量有限,所以用户趋之若鹜,在秒杀活动开始前涌入系统, 等到秒杀活动开始的一瞬间,点下购买按钮(在此之前购买按钮为灰色,不可以点击),抢购商品。

秒杀虽然对应用推广有很多好处,但是对系统技术却是极大的挑战:系统是为正常运营设计的,而秒杀活动带来的并发访问用户却是平时的数百倍甚至上千倍。也就是说,秒杀的时候,系统需要承受比平时多得多的负载压力。

为了应对这种比较特殊的营销活动,我们启动了一个专门的秒杀项目,项目代号是“Apollo”。Apollo的核心挑战是:如何应对突然出现的数百倍高并发访问压力,并保证用户只有在秒杀开始时才能下单购买秒杀商品?接下来我们就看看Apollo的需求与技术架构吧。

需求分析

Apollo的需求主要有两点。

  • 独立开发部署秒杀系统,避免影响现有系统和业务

秒杀活动只是网站营销的一个附加活动,这个活动具有时间短、瞬间并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个系统瘫痪。

而且由于秒杀时的最高并发访问量巨大,整个电商系统需要部署比平常运营多好几倍的服务器,而这些服务器在绝大部分时候都是用不着的,浪费惊人。所以秒杀业务不能使用正常的电商业务流程,也不能和正常的网站交易业务共用服务器,甚至域名也需要使用自己独立的域名。总之,我们需要设计部署专门的秒杀系统,进行专门应对。

  • 防止跳过秒杀页面直接下单

秒杀的游戏规则是:到了秒杀时间才能开始对商品下单购买。在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的 URL,如果得到这个 URL,不用等到秒杀开始就可以下单了。秒杀系统 Apollo 必须避免这种情况。

概要设计

Apollo要解决的核心问题有:

  1. 如何设计一个独立于原有电子商务系统的秒杀系统,并独立部署。
  2. 这个秒杀系统如何承受比正常情况高数百倍的高并发访问压力。
  3. 如何防止跳过秒杀页面获得下单URL。
    我们将讨论这三个问题的解决方案,并设计秒杀系统部署模型。

独立秒杀系统页面设计

秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是精细的商品描述等用户体验细节,因此秒杀系统的页面设计应尽可能简单。秒杀商品页面如图。

商品页面中的购买按钮只有在秒杀活动开始时才变亮,在此之前以及秒杀商品卖出后,该按钮都是灰色的,不可以点击。 秒杀时间到,购买按钮点亮,点击后进入下单页面,如图。

图片

下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给订单子系统,才能成功创建订单,其余用户提交订单后只能看到秒杀结束页面。

秒杀系统只需要设计购买和下单两个页面就可以了,因为不管有多少用户来参与秒杀,只有第一个提交下单的用户才能秒杀成功,因此提交订单并创单成功的用户只有一个,这个时候就没有什么高并发了。所以订单管理、支付以及其他业务都可以使用原来的系统和功能。

秒杀系统的流量控制

高并发的用户请求会给系统带来巨大的负载压力,严重的可能会导致系统崩溃。虽然我们设计并

部署了独立的秒杀系统,秒杀时的高并发访问压力只会由秒杀系统承担,不会影响到主站的电子商务核心系统,但是秒杀系统的高并发压力依然不容小觑。

此外,秒杀系统为了提高用户参与度和可玩性,秒杀开始的时候,浏览器或App并不会自动点亮购买按钮,而是要求用户不停刷新页面,使用户保持一个高度活跃的状态。但是这样一来,用户在秒杀快要开始的时候拼命刷新页面,会给系统带来更大的高并发压力。

我们知道,缓存是提高响应速度、降低服务器负载压力的重要手段。所以,控制访问流量、降低系统负载压力的第一个设计方案就是使用缓存。Apollo采用多级缓存方案,可以更有效地降低服务器的负载压力。

首先,浏览器尽可能在本地缓存当前页面,页面本身的HTML、JavaScript、CSS、图片等内容全部开启浏览器缓存,刷新页面的时候,浏览器事实上不会向服务器提交请求,这样就避免了服务器的访问负载压力。

其次,秒杀系统还使用CDN缓存。CDN即内容分发网络,是由网络运营服务商就近为用户提供的一种缓存服务。秒杀相关的HTML、JavaScript、CSS、图片都可以缓存到CDN中,秒杀开始前,即使有部分用户新打开浏览器,也可以通过CDN加载到这些静态资源,不会访问服务器,又一次避免了服务器的访问负载压力。

同样,秒杀系统中提供HTML、JavaScript、CSS、图片的静态资源服务器和提供商品浏览的秒杀商品服务器也要在本地开启缓存功能,进一步降低服务器的负载压力。

使用多级缓存的秒杀系统部署图如下。

图片

以上是针对秒杀开始前,缓存可以降低用户频繁刷新给服务器造成的流量压力。但是秒杀开始后,用户购买和下单的并发请求就不能使用缓存了,但我们仍然需要对高并发的请求流量进行控制。因此,秒杀开始后,秒杀系统会使用一个计数器对并发请求进行限流处理,如下图。

图片

因为最终成功秒杀到商品的用户只有一个,所以需要在用户提交订单时,检查是否已经有其他用户提交订单。事实上,为了减轻下单页面服务器的负载压力,可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户则直接进入秒杀结束页面。假设下单服务器集群有 10 台服务器,每台服务器只接受最多10个下单请求,这样整个系统只需要承受100并发就可以了,而秒杀成功的用户也只能出现在这100并发请求中。

事实上,限流是一种非常常用的高并发设计方案,我们会在下个模块专门设计一个通用的限流器。通过缓存和限流这两种设计方案,已经可以应对绝大多数情况下秒杀带来的高并发压力。

秒杀商品页面购买按钮点亮方案设计与下单URL下发

前面说过,购买按钮只有在秒杀活动开始时才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还是点亮。但是在前面的设计中,为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计成了静态页面,缓存在 CDN、秒杀商品服务器,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。

因此,我们需要在秒杀商品静态页面中加入一个特殊的 JavaScript 文件,这个JavaScript 文件设置为不被任何地方缓存。秒杀未开始时,该JavaScript文件内容为空。当秒杀开始时,定时任务会生成新的 JavaScript 文件内容,并推送到JavaScript服务器。

新的JavaScript文件包含了秒杀是否开始的标志和下单页面 URL 的随机数参数。当用户刷新页面时,新JavaScript文件会被用户浏览器加载,根据JavaScript中的参数控制秒杀按钮的点亮。当用户点击按钮时,提交表单的URL参数也来自这个JavaScript文件,如图。

图片

这个JavaScript文件还有一个优点,那就是它本身非常小,即使每次浏览器刷新都访问 JavaScript 文件服务器,也不会对服务器集群和网络带宽造成太大压力。

秒杀系统部署模型

综上设计方案,Apollo 系统整体部署模型如下。

图片

用户在浏览器打开秒杀商品页面,浏览器检查本地是否有缓存该商品信息。如果没有,就通过CDN加载,如果CDN也没有,就访问秒杀商品服务器集群。

用户刷新页面时,除了特殊JavaScript文件,其他页面和资源文件都可以通过缓存获得,秒杀没开始的时候,特殊JavaScript文件内容是空的,所以即使高并发也没有什么负载和带宽访问压力。秒杀开始时,定时任务服务器会推送一个包含点亮按钮指令和下单URL内容的新JavaScript文件,用来替代原来的空文件。用户这时候再刷新就会加载该新的JavaScript文件,使购买按钮点亮,并能进入下单页面。

下单URL中会包含一个随机数,这个随机数也会由定时任务推送给下单服务器,下单服务器收到用户请求的时候,检查请求中包含的随机数是否正确,即检查该请求是否是伪造的。

进入下单服务器的请求会被服务器进行限流处理,每台服务器超过10个的请求会被重定向到秒杀结束页面。只有前十个请求返回下单页面。用户填写下单页面并提交到下单服务器后,需要通过全局计数器进行计数。全局计数器会根据秒杀商品库存数量,确定允许创单的请求个数,超过这个数目的请求也将重定向到秒杀结束页面。最终只有有限的一个用户能够秒杀成功,进入订单处理子系统,完成交易。

小结

这个文档是根据某互联网大厂真实案例改编的。当年该厂为了配合品牌升级,搞了一次大规模的营销活动,秒杀是整个营销活动的一部分。运营团队在投放了大量广告并确定了秒杀活动的开始时间后才通知技术部:我们准备在一周后搞一个秒杀活动,预计参加秒杀的人数是正常访问人数的几百倍。

当时参加会议的架构师们面面相觑,时间太短,并发量太高,谁也不敢贸然接手。最后有个架构师站出来接手了这个项目,并最终完成了秒杀活动。此后,这名架构师成了公司的红人,短短几年晋升为集团副总裁,负责一个有十多亿用户、几乎所有中国人都耳熟能详的互联网应用。

我们现在重新把这个设计拿出来复盘,看起来技术含量也不过如此。那么如果把你放到当时的会议现场,你是否有勇气站出来说:“我来。”

对一个架构师而言,精通技术是重要的,而用技术建立起自己的信心,在关键时刻有勇气面对挑战更重要。人生的道路虽然漫长,但是紧要处可能只有几秒。这几秒是秒杀系统高并发访问高峰的那几秒,也是面对挑战迎难而上站出来的那几秒。

思考题

Apollo秒杀系统针对的是大量用户在短时间购买极少数商品的情况,通过限流器拦截大量用户请求,进而降低系统负载压力的设计思路。那么对于大量用户在短时间购买大量商品的情况,比如双十一这种电商大促场景,设计方案又非常不同,你有什么样的设计思路呢?

欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。

精选留言

  • 👽

    2022-03-05 23:41:45

    首先,我个人分析:
    大促跟秒杀系统最大的不同在于——秒杀大部份下单请求是被拒绝的。而双十一这种活动,是尽可能将所有请求都受理。否则就会出现损失。
    持续时长一般比秒杀业务长,秒杀的高并发可能是几分钟内的,而大促活动,可能持续几小时。
    并发数预测大幅度小于秒杀。

    基于以上的前提设计系统。
    首先,基础架构方面。大促持续时长虽然高于秒杀。但是仍然并非常态。所以,计算资源应该是支持弹性伸缩的。应对大促时,增加服务副本数量,平时将资源释放。
    cdn 静态资源缓存的设计可以沿用。
    网关层面,根据源IP或者用户控制请求频率。还是之前的问题,秒杀,你可以把处理不了的请求直接拒绝掉,但是大促情况下不可以。损失一个订单,就是一笔收益。所以根据ip或者用户,进行限流。
    为了节约短时间内的高并发请求,也可以尝试引入消息队列中间间,将部分请求先受理,排队处理,服务器空闲时候再处理这些业务。

    总结一下的话,核心就是:弹性伸缩,资源预留;用户限流,限制请求;短时间高并发,MQ削峰填谷;
    作者回复

    非常赞,思路很完整

    2022-03-07 11:03:53

  • Steven

    2022-03-09 15:06:29

    双十一这种电商大促场景,不同于秒杀场景,需要尽可能多的满足下单支付需求。

    虚拟化技术,容器技术来进行弹性伸缩,大促阶段增加资源,大促结束了释放。
    限流、熔断、降级等手段,以保证核心业务的可用性。
    MQ用来消峰填谷。
    多级缓存,尤其静态资源在CDN要大规模使用。

    另,非技术方面,比如:
    引导用户提前把要买的物品添加购物车,可以预估流量。
    个别商品、商家提前进入大促,也可以分流压力。
    作者回复

    2022-03-10 09:58:35

  • Rick

    2022-03-05 11:31:16

    请问一下为什么要使用动态生成js文件的方式来控制按钮点亮。为什么不直接使用http api请求直接从服务器端动态获取控制参数{是否开始秒杀了,提交请求的随机数等}。
    作者回复

    这个设计的场景主要是针对Web浏览器。App上的话api控制更好。

    2022-03-07 09:51:13

  • aoe

    2022-03-04 09:43:49

    秒杀都是套路,谁认真谁输
  • 华佗救不了你

    2022-03-10 01:10:39

    用来下发特殊js的那个接口,虽然占带宽不会高,但是QPS必然很高。而且还随着URL下发了一个随机数,显然这个请求不能简单地走缓存。即使可以走缓存,针对大流量是不是依旧需要部署大量的机器?
    老师可以说详细点这个点吗?
    作者回复

    是的,不能走缓存。
    js的响应时间是毫秒,单服务器QPS可以支持几万,完全可以承受。

    2022-03-10 15:51:31

  • neohope

    2022-04-29 15:06:00

    电商大促场景:
    0、尽早做好各相关团队的沟通,做好活动动员工作
    1、业务上分时段大促,不同商品分流,缓解服务器甚至快递小哥压力
    2、做好静态资源缓存:浏览器、CDN、服务器缓存
    3、根据过往流量、商品浏览量、购物车商品数量,估算大促流量
    4、做好扩容和运维保障工作:服务器、带宽、数据库根据预测流量,进行弹性扩容,而且要有一定比例的富余量
    5、大促商品信息提前加载redis缓存
    6、引入MQ,异步处理订单,消除业务峰值
    7、做好PlanB,限流、降级、熔断,保证核心业务稳定
    8、网关控制同IP流量,屏蔽部分服务器IP断,防止部分刷单行为
    9、提前做好性能测试及压力测试
    作者回复

    2022-05-05 11:35:48

  • 赵惠民

    2022-03-06 02:37:33

    给某些用户直接返回秒杀失败,不让他们参加
  • 喆里

    2022-06-15 17:49:03

    有个问题详请教下,客户端怎么知道秒杀活动什么时候开始? (两边的时间可能不一致)
    因此,是不是客户端每次刷新,还是会请求到js服务器,秒杀没开始时,响应是空白js;秒杀开始后,返回是真正有用的js ?
    作者回复

    是的

    2022-06-16 11:25:40

  • 子路

    2022-04-26 12:59:34

    秒杀更多思考是如何处理请求,库存很少,所以99%的请求都可以被抛弃,甚至它们都请求不到服务器,在网关或者负载阶段就被拒绝了,所以秒杀的服务器并没有承载的压力!
    作者回复

    是的

    2022-04-26 14:17:36

  • Geek_edffd3

    2022-04-21 23:45:49

    1.这里url动态化使用接口获取而不是通过js同步更简单?
    2.url动态化到底能起到多大作用呢?获取url可以通过脚本刷接口获得,可以更快地感知到活动开始和获取到url,也就意味着可以更早的去下单。所以,它真正起到了什么作用呢?
    3.其实,不动态url,可以钓鱼,如果活动开始前调用接口就可以直接拉入黑名单。
    4.关于按钮置灰,直接前端简单做个效果就好了,对于一般用户就是灰的,对于非一般用户,接口里面肯定会检验活动开始时间的,没开始也不可能成功的。
    老师,希望得到你的答疑解惑,谢谢🙏
    作者回复

    1 这个设计主要针对web浏览器场景的,只能JS
    2 有随机数,动态化主要目的是防止秒杀开始前下单。
    3 人也会调接口的,直接黑名单有点太粗暴。可以用类似区块链那种hash碰撞,要求客户端必须进行一个复杂的CPU计算才能请求后端,可以降低脚本机器人的的优势。

    2022-04-22 16:50:00

  • 1angxi

    2022-03-28 23:03:16

    19年的时候搞过秒杀,因此场次比较少,所以不需要考虑anti fraud。用了单机限流只让极少流量进入下单逻辑。这里没怎么讲库存扣减的逻辑,这也是一块比较核心的技术点,秒杀超卖是绝对不能接受的。
    作者回复

    秒杀最后进入正常的下单流程,下单流程包含了库存扣减。

    极少的流量进入下单逻辑,这个时候已经跟秒杀没关系了。

    2022-03-29 10:55:53

  • HappyHasson

    2022-03-19 23:31:46

    这里会有一个风险点吧。
    定时服务器在生成js脚本和随机值的时刻,想JavaScript服务器集群推消息,如果这时候某一台服务器网络有问题,没收到推送的消息,导致连到这台服务器的用户都看不到活动开始,等看到的时候已经结束了,这种情况玩家体验会很差吧。

    是否可以在JavaScript服务集群实例上自己起定时器,生成随机值给到client。随机种子一致,结果一致即可。
    作者回复

    嗯,你说的风险确实存在。这个风险可以通过一些高可用的运维手段来降低,但是风险还是会存在的。

    你的建议会带来另一个问题,这个随机种子谁来设定?如果这个种子被泄漏,有人在秒杀开始前生成随机值,抢了商品怎么办?如果秒杀的商品价值比较高,会涉及刑事调查,你作为架构师能承担这个责任吗?

    2022-03-21 11:30:36

  • Aaron

    2022-03-09 08:57:10

    老师,请教一下,在秒杀活动未开始之前,用户一直用的是缓存中的JavaScript,秒杀开始的时候会生成新的JavaScript,这个时候怎么能拿到可以秒杀的JavaScript?
    作者回复


    原文:
    因此,我们需要在秒杀商品静态页面中加入一个特殊的 JavaScript 文件,这个 JavaScript 文件设置为不被任何地方缓存。秒杀未开始时,该 JavaScript 文件内容为空。当秒杀开始时,定时任务会生成新的 JavaScript 文件内容,并推送到 JavaScript 服务器。

    2022-03-09 09:33:19

  • peter

    2022-03-04 19:54:52

    请教老师几个问题啊:
    Q1:秒杀系统独立部署,但秒杀是个短暂行为,秒杀结束后秒杀系统的服务器会被其他系统使用吗?(估计是秒杀系统专用吧)
    Q2:秒杀系统的服务器规模一般比较小吗?本文中的例子只用了不到三十台服务器,算是比较小的规模了。大厂的秒杀系统其服务器规模大约多少?比如阿里、京东这样的大厂。
    Q3:本文中的秒杀系统规模能是为多大规模的用户秒杀设计的?
    百万用户在线秒杀?
    Q4:秒杀与红包的区别是什么?(后面会讲红包系统吗?)
    Q5:浏览器查询自身缓存,是自己的行为还是需要服务器的控制?
    Q6:浏览器缓存-》CDN-》服务器,这个顺序是自动形成的?还是在服务器的控制下形成的?怎么控制的?
    Q7:定时任务服务器只产生一次javascriot和随机数吗?还是定时产生多次?
    Q8:秒杀开始前,秒杀按钮是灰色的,但按钮是在静态页面上,黑客能看到页面代码,那就可以修改页面代码,点亮按钮啊。这不就出现问题了吗?
    Q9:服务器没有用tomcat,为什么?
    系统用了apache、lighttp、jetty,但没有用tomcat,为什么?
    Q10:全局计数服务器是缓存,是用redis吗?
    作者回复

    1 看运营策略,如果秒杀经常发生,秒杀服务器会保留,不过服务器数量并不大,问题不大。反而是CDN,最好按带宽或者内存占用弹性租赁。

    2 秒杀的压力在请求接入环节,大量使用各种缓存,服务器规模并不大。秒杀压力和是否大厂并不完全相关,看运营活动的规模。

    3 这个系统最后的并发用户几万

    4 秒杀和红包,技术和业务上都关系不大

    5 http head可以设置缓存

    6 这个顺序就是请求经过的顺序,在每个环节设置缓存

    7 一次

    8 点亮按钮没用,没有请求URL,发不出请求,文中讨论过

    9 因为用了Jetty

    10 是的

    2022-03-07 11:00:04

  • 易企秀-郭彦超

    2022-03-04 08:31:54

    弱弱的问一句是京东吗
  • starj

    2022-03-11 18:58:28

    定时任务产生的javascript怎么保证秒杀开始那一瞬间准时上传?总会有延时的吧,这样秒杀就不是准点开始了
    作者回复

    既然说了定时任务,那就是定时上传了。采用每毫秒轮询一次的方式,定时器可以控制在毫秒级进行上传。
    秒杀开始的时间误差控制在一秒内就可以接受的。

    2022-03-14 11:24:00

  • 今天

    2022-03-05 09:50:21

    老师,非常想知道你用的画图软件是什么
    作者回复

    亿图图示,和www.draw.io~

    2022-03-06 10:41:46

  • Z.G

    2024-04-22 11:22:04

    老师好,有个问题希望能解惑,下单 URL 中会包含一个随机数,服务器收到用户请求的时候,检查请求中包含的随机数是否正确
    1. 是个实际意义有多大?机器人可以反复请求这个js文件,同样也能拿到这个随机数吧?
    2.服务器如何校验?另外还有一个表来存储随机数吗?
    作者回复

    1 随机数的用途 一是用来控制判断秒杀是否开始,文中有写到,二是用来防止内部人作弊,如果URL被内部人泄漏,依然可以秒杀未开始就抢走商品
    2 用缓存服务器存储

    2024-04-24 13:28:38

  • 周建勇

    2023-12-15 15:22:22

    需要用户手动来刷新,体验不会很差嘛?换成接口主动定时请求来点亮按钮会有什么挑战呢?只是查询时间判断是否可以开始,这种不会有什么压力吧?
    作者回复

    秒杀是要用户去秒杀的,游戏的玩法就是让用户参与,这个体验就是秒杀游戏的一部分。

    2023-12-19 20:19:37

  • 有思想的芦苇

    2022-10-23 23:04:26

    针对秒杀场景,除了李老师讲到的服务器端限流,是不是还可以考虑在客户端限流,客户端通过简单的算法,只允许很小的一个比例,比如1%的用户,才能向服务器端发起请求js文件或api接口。其他用户只是在等待秒杀结束,到时间后直接客户端重定向到秒杀结束页面。如果用户量足够大,客户端限流算法可以非常简单,不需要非常精确,也可以降低服务器的资源需求。