性能文章>如何设计一个秒杀系统>

如何设计一个秒杀系统原创

1年前
370433

文章导读

前言

说起秒杀,我想你肯定不陌生,从每年双 11、618 购物节,再到 12306 抢火车票,秒杀的场景处处可见。

我们就要想了:为什么需要秒杀系统?秒杀系统最大的优势是什么?

我们知道,电商平台的本质是在线上撮合买卖双方的购销需求,达成交易。虽然是线上交易,但也遵守朴素的经济学原理,供求关系决定了商品的经济活动

  • 当供求平衡时,买方和卖方处于对等关系,双方是相对稳定、和谐的;

  • 供大于求时,这时候市场成了买方市场,买方处于主动地位;

  • 供不应求时,这时候市场是卖方市场,卖方处于有利的主导地位。

买卖双方的关系不同,决定了电商平台会采用哪种营销方式。

  • 当供大于求,一般会采用各种促销让利的方式,吸引消费者购买,常见的促销方式有单品满减、总价优惠、赠品、会员优惠等;

  • 当供不应求,就需要有计划地设置营销活动、用户参与门槛以及限购规则,尽可能的让商家利益最大化,同时惠及更广泛的消费者。

所以说,为什么需要秒杀系统,其实很简单,那就是商品的极度供不应求

那么秒杀系统又有哪些优势呢?

  1. 对于这些极度供不应求的爆品,合理地设置活动,适时地释放库存,可以持续为电商平台带来稳定的热度和流量,可观的 VIP 会员费,以及技术口碑的认同。

  2. 通过秒杀系统,电商平台也可以对参与用户进行筛选,杜绝黄牛和刷子,让爆品惠及更广,更公平。

什么是秒杀

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

秒杀主要解决两个问题:一个是并发读,一个是并发写

秒杀的通用玩法

正常电子商务流程如下:

  1. 商品详情点击下单

  2. 创建订单

  3. 扣减库存

  4. 更新订单

  5. 付款

  6. 卖家发货

秒杀流程

  1. 活动一般都是定时开始,所以通常情况下,平台商家会拿出稀缺商品,事先在秒杀的运营系统中设置好活动的开始、结束时间,以及投入的库存(简单的玩法,只要这几个主要元素即可)。

  2. 活动开始,用户可以通过活动抢购入口(一个商品详情页,或是一个广告链接),进入到活动的结算页,然后点击下单,完成商品的抢购操作,整个过程如下:

这种方式通用性很强,可以适配大部分的平台。

如果想对流量有个预期上限,方便做备战工作,那么你可以加上预约功能,即在活动开始前,先开放一段时间的预约,让用户先去进行预约,然后才能获得参加抢购活动的资格。

如果面对的业务场景复杂些,你还可以联合风控,在参加活动时校验用户资质,踢掉黄牛以及有过不良行为的人,尽量将资源给到优质用户。

秒杀的业务特点

这里,我们可以使用 12306 网站来举例,每年春运时,12306 网站的访问量是非常大的,但是网站平时的访问量却是比较平缓的,也就是说,每年春运时节,12306 网站的访问量会出现瞬时突增的现象。

所以,秒杀系统的流量和并发量我们可以使用下图来表示。

 

通过上图可以看出,秒杀系统的并发量存在瞬时凸峰的特点,也叫做流量突刺现象

我们可以将秒杀系统的特点总结如下。

(1)限时、限量、限价

在规定的时间内进行;秒杀活动中商品的数量有限;商品的价格会远远低于原来的价格。

例如,秒杀活动的时间仅限于某天上午 10 点到 10 点半,商品数量只有 10 万件,售完为止,而且商品的价格非常低,例如:1 元购等业务场景。

限时、限量和限价可以单独存在,也可以组合存在。

(2)活动预热

需要提前配置活动;活动还未开始时,用户可以查看活动的相关信息;秒杀活动开始前,对活动进行大力宣传。

(3)持续时间短

购买的人数数量庞大;商品会迅速售完。

在系统流量呈现上,就会出现一个突刺现象,此时的并发访问量是非常高的,大部分秒杀场景下,商品会在极短的时间内售完。

一句话总结秒杀特点:活动持续时间短,商品数量少、价格低,参与人数多。

面临的技术挑战

 

 

 

(1)巨大的瞬时流量

秒杀活动的特点,就是将用户全部集中到同一个时刻,然后一起开抢某个热门商品,而热门商品的库存往往又非常少,所以持续的时间也比较短,快的话可能一两秒内就结束了。

活动开始一瞬间会有大量的用户流量涌入,流量可能是平时的几十倍,系统 QPS 非常高,因此对系统的性能要求非常高。

(2)对现有业务造成冲击

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

(3)热点数据问题

高并发下一个无法避开的问题,就是热点数据问题。

特别是对于秒杀活动,大家抢购的都是同一个商品,所以这个商品直接就被推到了热点的位置,无论是用的数据库,还是分布式缓存,都无法支持几十万、上百万对同一个 key 的读写,以 Redis 的写为例,最高仅可支持几万的 TPS。像商品库存的控制,就会有这个问题。

(4)前台用户频繁刷新,数据库的负载较高

用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。

(5)高并发下的数据安全问题

活动商品一般都会有库存限制,一定要控制好并发,不能因为高并发引发了库存超卖。

(6)刷子流量

一般我们提供的秒杀对外服务,都是 HTTP 的服务。不管你是用 H5 实现的页面,还是通过安卓或是 iOS 实现的原生页面,特别是 H5,都可以直接通过浏览器或是抓包工具拿到请求数据,这样刷子便可以自己通过程序实现接口的直接调用,并可以设置请求的频率。

这样高频次的请求,会挤占正常用户的抢购通道,同时,刷子也获得了更高的秒杀成功率。这不仅破坏了公平的抢购环境,也给系统服务带来了巨大的额外负担。

总结来说,瞬时的大流量是最大的挑战,当业务系统流量成几何增长时,有些业务接口加机器便可以支持。但考虑到成本与收益,在有限的资源下,如何通过合理的系统设计来达到预期的业务目标,就显得格外重要了。

清楚了秒杀系统所面临的挑战,接下来我们就可以考虑如何应对了。所谓知己知彼,百战不殆。

秒杀系统如何设计

在设计系统之前,我们先来看下,一次 HTTP 请求所经过的链路路径。

一次 HTTP 请求所经过的链路路径

如果我们提供的是一个 HTTP 服务,那么每个客户端请求进来都要经过这些链路,而每个链路节点的作用又是什么呢?我们逐一看下。

  • DNS:负责域名解析,会将你的域名请求指定一个实际的 IP 来处理(事先配置好处理请求的 IP,DNS 按顺序指定),并且一般客户端浏览器会缓存这个 IP 一段时间,当下次再请求时就直接用这个 IP 来建立连接,当然如果指定的 IP 挂了,DNS 并不会自动剔除,下次依然会使用它。

  • Nginx:也就是上面的被 DNS 指定来处理请求的 IP,一般都会被用来当做反向代理和负载均衡器使用,因为它具有良好的吞吐性能,所以一般也可以用来做静态资源服务器。当 Nginx 接收到客户端请求后,根据负载均衡算法(默认是轮询)将请求分发给下游的 Web 服务。

  • Web 服务:这个就是我们都比较熟知的领域了,一般我们写业务接口的地方就是这了,还有我们的 H5 页面,也都可以放到这里,这里是我们做业务聚合的地方,提供页面需要的数据以及元素。

  • RPC 服务:一般提供支撑业务的基础服务,服务功能相对单一,可灵活、快速部署,复用性高。RPC 服务一般都是公司内部服务,仅供内部服务间调用,不对外开放,安全性高。

用户的一次抢购和系统的交互

在了解了一次请求所经过的链路节点后,接下来我们再看下,在用户的一次抢购过程中,每次和系统的交互都要做什么事情。

 

 

结合上图来看,商详页部分和支付页部分,对于一般平台来说,都是通用板块,而从「点击抢购」开始到「下单成功待支付」,这一段是属于秒杀系统的业务范畴,在这里我们梳理下,有哪几件事情是需要秒杀系统来做的。

  1. 提供活动数据:提供参加秒杀活动的商品信息,主要用于商详页判断活动的倒计时、开始、结束等页面展示和抢购入口校验。

  2. 提供结算页:如果把秒杀做成一个单独业务模块,可跨平台(安卓、PC、iOS)嵌入,那么就需要提供一整套服务,包括 H5 页面,主要用于展示商品的抢购信息,包括商品名称、价格、抢购数量、地址、支付方式、虚拟资产等等。

  3. 提供结算页页面渲染所需数据:包括用户维度的地址、虚拟资产等数据,活动维度的名称、价格等数据。

  4. 提供下单:用户结算页下单,提供订单生成或是将下单数据透传给下游(如果平台有通用的订单接入接口)。当然在这中间,还有个隐形的,但却是非常重要的核心能力,那就是做流量的精细化筛选,尽量确保传给下游接口的流量,都是优质请求。

对于系统的设计,有一些基本的原则,比如校验前置、分层过滤,再结合我们上面的链路路径图,效果如下所示:

 

 

(1)一般我们会在 DNS 层做一些和网络相关的防攻击措施,公司的网络安全部门有统一的一些配置措施,这层我们无法写业务,但是可以拦截一些攻击请求。

(2)接下来到 Nginx 层。Nginx 不仅可以作为反向代理和负载均衡器,也可以做大流量的 Web 服务器,同时也是一款非常优秀的静态资源服务器。如果把业务校验也放到这里来,就可以实现校验前置的原则了吗?

Nginx+Lua 说,没问题。

这时候可能你会有顾虑,Nginx 担负了那么多任务,会被拖垮吗?

不会,因为 Nginx 很强大,能力越大,责任越大。

(3)接下来就到了 Web 服务了。我们在这里做业务的聚合,提供结算页页面渲染所需要的数据以及下单数据透传,同时也负责流量的筛选与控制,保证下游系统的安全。

(4)最后就是 RPC 服务。它提供基础服务,一般经过上面 3 层的严格把关,到这里的请求,量已经小很多了,我们写业务逻辑,在技术上也有更多的发挥空间。

秒杀系统机构设计

我们通常都说,没有最好的技术,只有最契合当下业务场景的技术,所以我们得先了解一下,如果使用我们传统的架构系统来支持秒杀业务,可能会出现哪些问题。只有清楚了要面对的问题,我们才能做针对性的思考和优化。

传统业务架构

这种功能结构以及系统架构,是我们非常熟悉的。在这种方式下,Nginx 只做反向代理和负载均衡,甚至这层对我们做业务开发的研发人员来说,都是无感知的,一般运维同事在做生产环境搭建时,都会帮我们配好。研发人员更多的是在开发 Web 服务和 RPC 服务,我们把页面以及页面所依赖的静态资源都放到 Web 服务中,同时 Web 服务还提供业务接口,RPC 服务提供一些支撑服务。

如果这是个 ToB 的运营管理系统,这样没有什么问题,因为请求量非常低,系统基本不会有太大的负载。但是对于 ToC,且瞬时流量非常大的情况,问题就会暴露出来,那它究竟会有哪些问题呢

域名与带宽问题

如果 Web 服务既提供 H5 页面、静态资源,同时也提供业务接口,这就意味着所有的请求使用的都是同一个域名,在活动刚开始时,大家都点击抢购按钮进结算页,而结算页页面拉取静态资源,会占用很多带宽资源。

这在活动开始的瞬间,带宽资源很稀缺的情况下,可能会出现用户进不了结算页,或者进了结算页却不能正常渲染页面的问题,导致抢购体验大幅下降。

Web 服务器性能问题

接着,讲一个关键问题。我们一般部署 Web 服务,都是使用 Apache 的 Tomcat 来部署的,Tomcat 在处理请求的时候,是通过线程去处理的。

这样的问题就是如果瞬时的大量请求过来,线程池中的线程不够用,Tomcat 就会瞬间新建很多线程,直至达到配置的最大线程数,如果线程数设置的过大,这个过程可能会直接将机器的 CPU 打满,导致机器死掉。

即使没有挂掉,在高负载下,当设置的等待队列也满了之后,后面的请求都会被拒绝连接,直到有空出的资源去处理新请求。

这时候你可能会想,我加机器分摊流量不就行了?可以是可以,但由此增加的活动成本不知道你的老板会不会买单?

当然了,这个过程中,还会伴有热点数据读写库存超卖等问题,这些细节也非常重要。

新的秒杀系统架构

首先新架构我们依然保留了 HTTP 服务常用的层级调用关系,即 Nginx → Web 服务 → RPC 服务,这也是绝大部分公司都会使用的一种系统结构。

其次将原先由 Web 服务提供的静态资源放到了 CDN(CDN 是全国都有的服务器,客户端可以根据所处位置自动就近从 CDN 上拉取静态资源,速度更快),大大减轻抢购瞬时秒杀域名的负担。

最后,同时也是我们所做的最大改变,就是将 Nginx 的职责放大,前置用来做 Web 网关,承担部分业务逻辑校验,并且增加黑白名单、限流和流控的功能,这其实也是考虑到我们的秒杀业务特点所做的调整。

这种在 Nginx 里写业务的做法在很多大公司里都是很常见的,像京东是用来做商详、秒杀的业务网关,美团用来做负载均衡接入层,12306 用来做车票查询等等,他们的共同特点都是要面对高并发的业务场景,这也说明在这种业务场景下,我们的设计是得到了真实实践和广泛认可的。

而这么做的目的,就是要充分利用 Nginx 的高并发、高吞吐能力,并且非常契合我们秒杀业务的特点,即入口流量大。

但流量组成却非常的混杂,这些请求中,一部分是刷子请求,一部分是无效请求(传参等异常),剩下的才是正常请求,这个的比例可能是 6:1:3,所以需要我们在网关层尽可能多地接收流量进来,并做精确地筛选,将真正有效的 3 成请求分发到下游,剩余的 7 成拦截在网关层。不然把这些流量都打到 Web 服务层,Web 服务再新起线程来处理刷子和无效请求,这是种资源的浪费。

所以网关层对秒杀系统而言,至关重要,而 Nginx 刚好可以胜任此项任务。

秒杀业务流程梳理

根据我们之前对秒杀业务的介绍,一场完整的秒杀活动的大概流程是这样的,我们一起梳理一下。

  1. 运营人员在秒杀系统的运营后台,根据指定商品,创建秒杀活动,指定活动的开始时间、结束时间、活动库存等。

  2. 活动开始之前,由秒杀系统运营后台 worker,将活动商品的标识更改为秒杀标识。

  3. 用户进入到商详页面时,系统会判断当前商品标识,如果是秒杀标识,则去查询当前商品的秒杀活动数据,判断是否正式开始,即通过商品标识 + 活动时间来判断活动是否真正开始。如果活动时间还没有到,页面可以是禁售展示,也可以是倒计时展示,或者是按正常价格售卖,这个可以按实际业务需求来定。

  4. 当活动已经开始,用户进入商详页,可以看到立即抢购的按钮,这里我们可以通过增加一些逻辑判断来限制按钮是否可以点击,比如是否设置了抢购用户等级限制,是否还有活动库存,是否设置了预约等等。如果都没限制,用户可以点击抢购按钮,进入到秒杀结算页。

  5. 在结算页,用户可更改购买数量,切换地址、支付方式等,这里的结算元素也需要按实际业务来定,更复杂的场景还可以支持积分、优惠券、红包、配送时效等,并且这些都会影响最终价格的计算。

  6. 确认无误后,用户提交订单,在这里后端服务可以调用风控、限购等接口,来完善校验,都通过之后,完成库存的扣减和订单的生成。如果结算页支持了第 5 步中提到的一些虚拟资产,则还需要做对应的抵扣。

  7. 订单完成后,根据用户选择的支付方式跳转到对应的页面,比如在线支付就跳转到收银台,货到付款的话,就跳到下单成功提示页。

 

这样一来,秒杀业务从开始到用户抢购,到最后的活动结束关闭,整个流程就形成闭环了。当然上面列举的也只是主要的流程,实际业务可以在不同节点依据实际需求添加不同的业务功能,这个你可以灵活调整。

系统提供接口梳理

通过上面的时序图中,我们可以非常清楚地归纳出秒杀系统需要提供的主要接口:

  1. 活动数据查询接口:查询活动相关信息,包括开始、结束时间等。

  2. 进结算页页面(H5)接口:结算页 H5,并通过 Ajax 异步加载结算页数据。

  3. 结算页页面初始化渲染所需数据的接口:大体包括活动信息、商品信息、结算信息(用户的地址、虚拟资产、价格等等)。

  4. 结算页页面用户行为操作接口:支持地址列表查看和选择,虚拟资产的查看和使用等等,并在操作后更新页面价格相关信息。

  5. 结算页提交订单接口:支持秒杀活动商品下单。

当然这是秒杀网关系统所需要提供的接口,但是要完整地实现整个秒杀功能,我们还得需要以下功能。而这些个功能点,不需要做到秒杀的主流程系统里,一般都有秒杀的运营系统来提供相应能力,简列如下:

  1. 秒杀活动的创建:创建秒杀活动,主要要素包括活动名称、参加活动的商品、活动库存、活动单次限购数量、活动开始时间、活动结束时间。

  2. 秒杀活动的查看:查看活动信息、活动状态等。

  3. 秒杀活动的开始:一般活动都是提前创建,并在活动即将开始之前几分钟,自动更改活动商品标识,这样商详页就能区分出当前商品是普通商品还是秒杀商品了,然后执行不同的业务分支逻辑。

  4. 秒杀活动的结束:活动时间到期或者运营人员手动关闭,并将活动商品的秒杀标识去掉。

我们针对秒杀流程做了详细的梳理,画出了系统间的交互过程,同时明确列出了整个秒杀系统所需要提供的主要接口和能力。

下面我们简要分析一下以上提供的每个接口的瓶颈点在哪里,会出现什么问题,以及如何有效应对了。

秒杀的高可用建设

我们上面介绍到秒杀的特点以及挑战提到,秒杀商品的数量是限量的,而且和普通商品的数量级是完全不同的。在头部电商平台,几十亿的商品都是普通商品,只有少数(百个以下)的商品具备秒杀商品的特点。

面对这样的区别,我们把这两类商品放在一块交易,显然是不合适的。为了不让 0.001% 的爆品影响 99.999% 普通商品的交易,我们就需要进行隔离。

秒杀的隔离策略

秒杀的隔离一般分成三个层面:

  • 业务隔离:把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。

  • 系统隔离:系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。

  • 数据隔离:秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01% 的数据有机会影响 99.99% 数据。

通过对秒杀流量的隔离,我们已经能够把巨大瞬时流量的影响范围控制在隔离的秒杀环境里了。接下来,我们开始考虑隔离环境的高可用问题。也就是说,普通商品交易流程保住了,现在就要看怎么把秒杀系统搞稳定,来应对流量冲击,让秒杀系统也不出问题。

 

方法很多,有流量控制、削峰、限流、缓存热点处理、扩容、熔断等一系列措施。

下面先来看流量控制。

秒杀的流量管控

在库存有限的情况下,过多的用户参与实际上对电商平台的价值是边际递减的。

 

举个例子,1 万的茅台库存,100 万用户进来秒杀和 1000 万用户进来秒杀,对电商平台而言,所带来的经济效益、社会影响不会有 10 倍的差距。

相反,用户越多,一方面消耗机器资源越多;另一方面,越多的人抢不到商品,平台的客诉和舆情压力也就越大。

当然如果为了满足用户,让所有用户都能参与,秒杀系统也可以通过堆机器扩容来实现,但是成本太高,ROI 不划算,所以我们需要提前对流量进行管控。

如果你关注过电商平台的双 11 或 618 大促,你肯定能感受到“预约 + 秒杀”在大促时的主流营销玩法。

  • 预约期内,开放用户预约,获取秒杀抢购资格;

  • 秒杀期内,具备抢购资格的用户真正开始秒杀。

在预约期内,关键是锁定用户,这也是我们能够用来做流量管控的核心。

秒杀的削峰和限流

下面我们从技术角度来讨论秒杀系统的其他高可用手段——削峰和限流,通过削峰,让系统更加稳健。

削峰的方法有很多,可以通过业务手段来削峰,

  • 比如秒杀流程中设置验证码或者问答题环节;

  • 也可以通过技术手段削峰,比如采用消息队列异步化用户请求,或者采用限流漏斗对流量进行层层过滤。

削峰又分为无损和有损削峰。本质上,限流是一种有损技术削峰;而引入验证码、问答题以及异步化消息队列可以归为无损削峰。

我们上面也提到了秒杀的流量突刺现象,就是毛刺特别大,流量几秒内爬升到峰值,然后马上掉下来。

 

 

我们现在需要做的就是通过削峰和限流,把这超大的瞬时流量平稳地承接下来,落到秒杀系统里。

流量削峰策略

  • 验证码和问答题:让用户在秒杀前输入验证码或者做问答题,不同用户的手速有快有慢,这就起到了让 1s 的瞬时流量平均到 30s 甚至 1 分钟的平滑流量中,这样就不需要堆积过多的机器应对 1s 的瞬时流量了。

  • 消息队列:通过异步消息队列,把原本的服务间直接调用进行解耦,让超过服务的流量,暂存在消息队列里。

  • 限流:系统自我保护的最直接手段,再厉害的系统,总有所能承载的能力上限,一旦流量突破这个上限,就会引起实例宕机,进而发生系统雪崩,带来灾难性后果。常见的限流手段:nginx 限流、线程池限流、API 限流:手写一个基于令牌桶的限流注解和实现。

降级、热点和容灾处理

当秒杀活动开启,流量洪峰来临时,交易系统压力陡增,具体表现一般会包括 CPU 升高,IO 等待变长,请求响应时间 TP99 指标变差,整个系统变得越来越不稳定。为了力保核心交易流程,我们需要对非核心的一些服务进行降级,减轻系统负担,这种降级一般是有损的,属于“弃卒保帅”。

而秒杀的核心问题,是要解决单个商品的高并发读和高并发写的问题,这是典型的热点数据问题,我们需要有相应的机制,避免热点数据打垮系统。

降级一般是有损的,那么必然要有所牺牲,下面介绍几种常见的降级手段:

  • 写服务降级,牺牲数据一致性获取更高的性能:由同步写数据库降级成同步写缓存、异步写数据库

  • 读服务降级,故障场景下紧急降级快速止损:通过建立多个缓存副本,或者除了增加 Redis 缓存之外,又增加了 ES 缓存,当秒杀的 Redis 缓存出现故障时,我们就可以通过降级开关,快速将读请求降级到 ES 上。

  • 简化系统功能,干掉一些不必要的流程,舍弃非核心功能。

打造不超卖和公平的秒杀系统

防刷

通过前面对秒杀业务的介绍,我们知道,秒杀系统之所以流量高,主要是因为一般使用秒杀系统做活动的商品,基本都是稀缺商品。稀缺商品意味着在市场上具有较高的流通价值,那么它的这一特点,必定会引来一群“聪明”的用户,为了利益最大化,通过非正常手段来抢购商品,这种行为群体我们称之为黑产用户。

 

黑产对秒杀业务的威胁是巨大的,它不仅破坏了公平的抢购环境,而且给秒杀系统带来了庞大的性能开销,所以我们的程序必须考虑应对策略。

黑产流量的特点是比正常流量快频率高,那么我们也就可以从这两个方面来着手思考对策。

只针对第一个快的特点,其实在活动开始后,进来的流量我们都无法将其定义为非法流量,这个只能借助像风控这种多维度校验,才能将其识别出来,除非它跳步骤。

而第二个高频率的特点,同时也是对秒杀系统造成危害最大的一种,我们还是有很多种手段来应对的。

  • Nginx 有条件限流:比如,根据用户 ID 来做限流,限流的速率为同一个用户 1 秒内只允许 1 个请求通过。

  • Token 机制:对于有先后顺序的接口调用,我们要求进入下个接口之前,要在上个接口获得令牌,不然就认定为非法请求,同时可以在 Nginx 层做 Token 的生成与校验,可以做到对业务流程主数据的无侵入。

  • 黑名单机制:黑名单机制分为本地黑名单和集群黑名单两种,这里单指本地黑名单。自己实现一套“逮捕机制”,即利用 Lua 的共享缓存功能,去统计 1 秒内这个用户或者 IP 的请求频率,如果达到了我们设定的阈值,我们就认定其为黑产,然后将其放入到本地缓存黑名单。黑名单可以被所有接口共享,这样用户一旦被认定为黑产,其针对所有接口的请求,都将直接被全部拦截,实现刷子流量的 0 通过。

  • 风控机制

秒杀扣减库存

减库存方式一般有三种:

  • 下单减库存

  • 付款减库存

  • 预扣库存

 

在大型秒杀中一般选择下单减库存方式。

库存的扣减主要涉及到两个核心操作:

  • 一个是查询商品库存,

  • 另一个是在活动库存充足的情况下,做对应数量的扣减。

两个操作拆分开来,都是非常简单的操作,但是在高并发场景下,不好的事情就发生了。

举个简单的例子,比如现在活动商品有 2 件库存,此时有两个并发请求过来,其中请求 A 要抢购 1 件,请求 B 要抢购 2 件,然后大家都去调用活动查询接口,发现库存都够,紧接着就都去调用对应的库存扣减接口,这个时候,两个都会扣减成功,但库存却变成了 -1,也就是超卖了。

 

从图中我们可以看到,库存超卖的问题主要是由两个原因引起的,一个是查询和扣减不是原子操作,另一个是并发引起的请求无序。

所以解决超卖问题的核心:做到库存扣减的原子性和有序性

 

 

我们在业务中经常会用到分布式锁,来控制多个进程对资源的访问。即通过 Redis 或者 ZooKeeper 来实现一个分布式锁,以商品维度来加锁,在获取到锁的线程中,按顺序去执行商品库存的查询和扣减,这样就同时实现了顺序性和原子性。

这个思路是可以的,只是不管通过哪种方式实现的分布式锁,都是有弊端的。

以 Redis 的实现来说,仅仅在设置锁的有效期问题上,就让人头大。

  • 如果时间太短,那么业务程序还没有执行完,锁就自动释放了,这就失去了锁的作用;

  • 而如果时间偏长,一旦在释放锁的过程中出现异常,没能及时地释放,那么所有的业务线程都得阻塞等待直到锁自动失效,这与我们要实现高性能的秒杀系统是相悖的。

所以通过分布式锁的方式可以实现,但不建议使用。

那还有其他方式吗?有!

解决方案:Redis + Lua。

利用 Redis 的单线程原理,天生就可以支持操作的顺序性,以及通过调用Lua脚本,保证脚本中的所有逻辑会在一次执行中按顺序完成。这样就能同时满足顺序性和原子性的要求了。

-- 调用Redis的get指令,查询活动库存,其中KEYS[1]为传入的参数1,即库存key
local c_s = redis.call('get', KEYS[1])
-- 判断活动库存是否充足,其中KEYS[2]为传入的参数2,即当前抢购数量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
   return 0
end
-- 如果活动库存充足,则进行扣减操作。其中KEYS[2]为传入的参数2,即当前抢购数量
redis.call('decrby',KEYS[1], KEYS[2])

参考资料

极客时间《手把手带你搭建秒杀系统》

 

 

点赞收藏
分类:标签:
ShawnBlog

我是 Shawn 一 Java 后端开发。欢迎关注我的公众号「ShawnBlog」。

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
3
3