如何设计一个秒杀系统

如何设计一个秒杀系统

秒杀已经成为电商不可缺少的一步分了,所谓 买到就是赚到,可以成功吸引到一大堆用户,那程序员面对这些用户该怎么办呢。我们该如何设计秒杀呢?

在设计秒杀前,我们需要先理解秒杀,秒杀其实主要就是解决两个问题:并发读并发写

  • 并发读的核心优化理念是尽量减少用户到服务端来 "读" 数据,或者让他们读更少的数据;
  • 并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。

另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。

然后我们升级到架构层面看,就是需要保证架构的几个常见的质量属性:高性能高可用一致性

  • 高性能

秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。我们从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化等。

  • 一致性

秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为 拍下减库存 付款减库存,在大并发更新的过程中都要保证数据的准确
性。

  • 高可用

虽然我们可以对系统进行优化,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然可以让我们不被开除。

初期架构轮廓

如何构建一个超大流量并发读写、高性能,以及高可用的系统,4要1不要

数据要尽量少

所谓 数据要尽量少,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。

因为首先这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU ,所以减少传输的数据量可以显著减少 CPU 的使用。例如,我们可以简化秒杀页面的大小,去掉不必要的页面装饰效果,等等。

其次,数据要尽量少 还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。调用其他服务会涉及数据的序列化和反序列化,而这也是 CPU 的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,数据越简单、越小则越好。

请求数要尽量少

用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求.

比如说,这个页面依赖的 CSS/JavaScript图片,以及 Ajax 请求等等都定义为额外请求,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久,减少请求数可以显著减少以上这些因素导致的资源消耗。

例如,减少请求数最常用的一个实践就是合并 CSSJavaScript 文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL ,然后动态把这些文件合并起来一起返回。

路径要尽量短

所谓 路径,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
通常,这些节点可以表示为一个系统或者一个新的 Socket 连接(比如代理服务器只是创建一个新的 Socket 连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。

然而,每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的话,那么整个请求的可用性是:99.9%5 次方,约等于 99.5%
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能 减少中间节点可以减少数据的序列化与反序列化,并减少延时(可以减少网络传输耗时)。
要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用RPC 变成 JVM 内部之间的方法调用。

依赖要尽量少

所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息用户信息,还有其他如优惠券成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。

要减少依赖,我们可以给系统进行分级,比如 0 级系统1 级系统2 级系统3 级系统0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。
注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。

不要有单点

系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是 消除单点

  • 那如何避免单点呢?

我认为关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。

  • 如何那把服务的状态和机器解耦呢?这里也有很多实现方式

例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。
应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景一般要通过冗余多个备份的方式来解决单点问题。

如何才能做好动静分离

静态资源压缩+cdn+缓存

在商家创建完秒杀产品后, 缓存就有了, 如果修改内容, 就直接更新缓存, 秒杀开始后, 商家就不能再修改了

有针对性地处理好系统的"热点数据"

为什么要关注热点

首先,热点请求会大量占用服务器处理资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占 90% 的服务器资源,如果这个热点请求还是没有价值的无效请求,那么对系统资源来说完全是浪费。

热点数据 静态和动态

静态热点数据是可以预知的,例如一直去请求秒杀的商品都是相同的。
动态数据指某些数据并非我们提前知道的,例如某个商品由于某个娱乐新闻成为top 1,这个时候商品的所带来的请求也会是top 1, 对于这种动态的热点数据我们没有办法预知,只能做好限制和保护等操作

最重要最简单的方式就是独立出来一个集群,单独处理热点数据。

流量削峰这事应该怎么做?

实际上在现实生活中也有削峰的例子,例如北京开车限号、进京证、限时限行

方案有:

  • 排队 上游洪水堆积,下游平稳放水,防止洪水伤害服务器
  • 答题 抢购前先答题,实际上就类似于验证码,但不是验证码,现在验证码识别率高,防止非正常用户的请求造成服务器的过高负载,防作弊、延缓请求
  • 分层过滤,每一层设置一些条件,可以是随机数、或者秒杀资格

减库存

付款减库存

即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
还有一个就是第三方支付的异步机制,有可能支付后没有库存,需要给用户退款

选用这种方案就需要牺牲用户的体验

下单扣库存

即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。

设置订单有效时间,但是还会存在恶意下单,我们可以采用一些方案来进行制止

例如,给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。还可以采用定金的营销形式来减少抢购时的流量

下单减库存 在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:

  • 设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错
  • 悲观锁
  • 乐观锁 版本号机制
  • 再有一种就是使用 sql 判断语句,例如这样的 SQL 语句
udpate goods set available = available - 1 where id = xx and available - 1 >= 0 ;

为了提高吞吐量,我们还可以根据商品 ID 进行分库分表设计, 将压力分布到其他的服务器; 还可以提前下好订单,将订单写入到 redislist ,然后来一个 pop 一个

  • 使用缓存处理库存

如果库存放缓存,缓存必须是高可用的,数据丢失怎么办,所以必须要多机房备份或者限流保护,如果出现极端情况,应当立马下架该商品

利用缓存来减轻数据库的压力,可以在缓存扣除后通过消息修改数据库,也或者设置多少时间去统一同步数据库

兜底方案的设计

没有人可以提前预估到所有情况

  • 架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。
  • 编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异- 常进行捕获,对无法预料的错误要有默认处理结果。
  • 测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
  • 发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。
  • 运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
  • 故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。

降级

所谓降级,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。

  • 降级方案可以这样设计

当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。从 20 改到 5 这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
是一个不得已而为之的举措。

限流

客户端限流,好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。
服务端限流,好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。

例如我们的系统最高支持 1w QPS 时,可以设置 8000 来进行限流保护。

拒绝服务

如果限流还不能解决问题,最后一招就是直接拒绝服务了。
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。

拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。

结语

我们针对不同场景有不同的架构,任何系统并非一蹴而就,当用户量不大的时候缓存+数据库就可以解决大量请求,我们还需要对自己系统的性能有个清晰的认识,每秒的 qps tps需要尽可能准确,做架构时就可以做出合适的方案。

如果当前设计满足不了业务的时候,我们需要对架构进行升级,可以将秒杀系统设计成为一个单独的系统,然后使用负载均衡分摊请求

对于秒杀的场景来说,不同 QPS 量级下瓶颈也会不一样,10w 级别可能瓶颈就在数据读取上,通过增加缓存一般就能解决,如果要到 100w 那么,可能服务端的网络可能都是瓶颈,所以要把大部分的静态数据放到 cdn 上甚至缓存在浏览器里

所以要做架构升级,还是主要要分析在预估的 QPS 下,整个系统的瓶颈会在什么地方,要针对这起瓶颈来重新设计架构方案

极客时间 如何设计一个秒杀系统

您的支持是对我最大的鼓励!

发表于: 作者:憧憬。
关注互联网以及分享全栈工作经验的原创个人博客和技术博客,热爱编程,极客精神
Github 新浪微博 SegmentFault 掘金专栏