likes
comments
collection
share

从零设计一个千万级流量的秒杀系统

作者站长头像
站长
· 阅读数 11

什么是高并发系统

在每年的京东“618”及淘宝的“双11”活动中,平台商家会有很多促销商品,而且商品价格对于用户来说吸引力巨大。面对这么巨大的流量,技术上如何保证这些商品不被“超卖”,同时还能给用户一个良好的购物体验?

12306 网站在刚开始对外在线售票时,经常出现系统瘫痪的现象,后来经过系统优化,现在已经可以支持上干万人同时抢票且不损害系统本身。

这些技术的背后都离不开高并发技术,需要利用高并发技术的方法论及设计原则,再结合业务本身进行架构设计,以应对系统面临的流量冲击

什么是高并发

  • 高并发(High Concurrency),通常是指通过设计保证系统能够同时处理很多请求。即在同一个时间点,有很多的请求同时访问同一个接口

  • 高并发意味着大流量,需要运用技术手段去抵抗这种大流量的冲击,以达到系统能平稳处理流量且系统自身依然运行良好的目的。现如今,高并发场景无处不在,例如京东的“618”、淘宝的“双 11”、热门车次车票的开售,以及各种电商秒杀抢购活动等。

  • 高并发是一种系统在运行过程中“短时间内遭遇大流量冲击”的情况。如果没有处理好,则很有可能造成系统吞吐量下降响应变慢,从而影响用户体验,甚至可能造成系统彻底不可对外服务的情况发生。所以,需要优化系统(包括硬件、网络、应用、数据库等)来达到高并发的要求。

高并发系统有哪些关键指标

设计一个系统或在对已有系统进行性能评估时,需要具备相应的参考指标,然后基于这些参考指标对系统进行针对性的优化,使得系统更健壮,以及具备更高的性能。接下来看看高并发系统都需要关注哪些关键指标。

响应时间(Response Time)

响应时间,是指从第一次发出请求到收到系统完整响应数据所需的时间。响应时间是反映系统性能的重要指标,直接反映了系统响应的快慢。

  • 从用户角度出发,响应时间决定着用户的体验感:响应时间越长,用户体验感越差,就会造成用户的流失;响应时间越短,用户体验感越好,有助于提高用户留存率。
  • 从系统本身的角度出发,响应时间决定着系统的性能问题:响应时间越短,系统性能越高,即能更好地处理业务;响应时间越长,系统性能越差,甚至可能会丢失相关请求或者出现系统不可用,从而影响公司业务。

吞吐量(Throughput)

吞吐量指单位时间内系统所处理的用户请求数

  • 从业务角度看,吞吐量可以用“请求数/秒”“人数/天”或“处理业务数/小时”等单位来衡量。
  • 从网络角度看,吞吐量可以用“字节数秘”来衡量。

对于互联网应用来说,吞吐量能够直接反映系统的负载能力。

采用不同方式表达的吞吐量,可以说明不同层次的问题,如:采用“请求数/秒”方式的吞吐量,则说明瓶颈主要来源于应用服务器和应用本身;

采用“字节数/秒”方式的吞吐量,则可以说明瓶颈主要来源于网络基础设施、服务器架构和应用服务器约束等。

在没有遇到性能瓶颈时,吞吐量与虚拟用户数之间存在一定的联系,可以采用以下公式计算吞吐量:F=VU × R / T

其中,F表示吞吐量,VU 表示虚拟用户个数,R表示每个虚拟用户发出的请求数,T表示性能测试所用的时间。

每秒请求数(QPS)

QPS 指服务器在一秒内共处理了多少个请求,主要用来表示“读”请求。

在系统上线前,一般怎么预估系统 QPS呢?绝大部分系统在白天的请求量都较大,所以,这里假设以白天来计算 QPS。依据三八原则,80%的流量是在 20%的时间段内产生的。

例如,现在每天有 5 000 000 个请求,预估 QPS =(5000000 ×0.8) /(12 × 60 × 60 × 0.2) = 462。即当前系统每天平均 QPS 为462。当然,为了保险起见,再预留个 20%左右也是可以的。一般还需要计算当天最高 QPS,这样对系统的掌控力度更强。

系统最高 QPS,可以通过系统平均 QPS的倍数计算出来。例如,分析业务得到最高 QPS大概是平均 QPS 的2倍,则当前系统峰值 QPS 为924左右。

在预估出 QPS 后,用“峰值 QPS/单台机器最高可承受的 QPS”就能计算出需要部署多少台服务器。即:

机器数 = 峰值 QPS / 单台机器最高可承受的 QPS

单台最高可承受的 QPS 可以通过压测来得出。假设单台机器压测得出最高可承受的 QPS为100,则所需要的机器数量为:924/100≈10台。

每秒事务数(TPS)

TPS即服务器每秒处理的事务数。TPS包括以下3个过程:

(1)客户端请求服务端。

(2)在服务端内部进行业务逻辑处理。

(3)服务端响应客户端。

一个事务包括“客户机向服务器发送请求 +服务器响应”的过程。在客户机发送请求时开始计时,在客户机收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

TPS 与 QPS 区别是什么呢?QPS类似于 TPS,但也有不同之处。例如:当用户访问一个完整页面时,请求+响应的整个过程就是一个 TPS;但是,这一次完整的页面请求可能产生多次对服务器的请求也就是一个页面请求了多个接口,这每请求一次就相当于一次 QPS。

访问量(PV)

PV (Page View)指页面浏览量。用户每对网站中的1个网页访问1次均被记录1次。用户对同一个页面的多次访问被累计记录。PV 是评价网站流量最常用的指标之一。

独立访客(UV)

UV(Unique Visitor)指访问某个站点或点击某个链接的不同IP 地址数。

在同一天内,UV 只记录第一次进入网站的具有独立IP地址的访问者,在同一天内访问者再次访问该网站则不计数。独立IP地址访问者相当于携带了“身份证”进入网站,它能反映在一定时间内有多少独立客户端进行了访问。

一个 UV 可以有很多个PV,一个 PV 也可以对应一个IP。例如,对网站访问一次,则网站的UV 就加1;这一次访问了该网站的两个页面,则网站的PV加 2;对其中一个网页又刷新了一次,则 PV再加1。

网络流量

因为受限于带宽,所以网络流量(也简称流量)是并发情況的一个重要指标,主要涉及以下两方面。

  • 流入流量:从外部访问服务器所消耗的流量。
  • 流出流量:服务器对外响应的流量。

从零设计一个千万级流量的秒杀系统

“秒杀”这个词在电商行业中出现的频率较高,如京东或者淘宝平台的各种“秒杀”活动,最类型的就是“双11抢购”。“秒杀”是指在有限的时间内对有限的商品数量进行抢购的一种行为,这是商家以“低价量少”的商品来获取用户的一种营销手段。

例如,用户数在千万级别的电商平台在上午10点10分10秒 开始1元抢购华为手机,手机只有10台。活动只要一开始,就会涌来大流量抢购华为手机,这对系统就是一个很大的挑战。

干万级流量“秒杀”系统架构一览

常用的秒杀系统架构图

从零设计一个千万级流量的秒杀系统

上图所示的架构比较简洁,主要分为以下5层。

  • 用户层:用户端的展现部分,主要涉及商品的相关信息及当前“秒杀”活动的信息。
  • CDN 层:缓存“秒杀”活动的静态资源文件。
  • 负载均衡层:拦截请求及分发路由等。
  • 服务层:“秒杀”活动的相关逻辑处理。
  • 基础设施层:数据存储、大数据计算及消息推送相关操作。

“秒杀”系统的特点

(1)“秒杀”系统的业务特点。

在“秒杀”活动还没开始时,流量一直是很平稳的状态;当“秒杀”活动开始时,系统流量呈直线突增;在“秒杀”活动结束后,流量又会急速下落,如图。

从零设计一个千万级流量的秒杀系统

如图可看出,“秒杀”活动的并发量在短时间内猛增,即瞬时并发量达到高峰。“秒杀”舌动有以下3个特点。

  • 限时、限量、限价。

“秒杀”活动在规定的时间开始:参与“秒杀”活动的商品数量较少,有些活动的商品数量在个立数;参与“秒杀”活动的商品的价格一般都比商品的原始价格低很多。

  • 活动预热。

对于确定的“秒杀”活动,在活动开始之前,需要对活动进行运营宣传,以及对活动页进行各中配置、准备活动详情页、预热活动静态文件等。

  • 持续时间短。

因为“秒杀”的商品数量很少,且流量极大,所以,“秒杀”活动持续时间不会太长,热门商品的“秒杀”活动可能只有1~2s。

(2)“秒杀”活动的技术特点。

“秒杀”活动的两个技术特点如下:

  • 瞬时并发量高:“秒杀”活动商品限时、限量、限价的特性,决定了“秒杀”活动一旦开始就会出现流量洪峰,即瞬间并发量达到峰值。
  • 并发读写:“读”比“写”要多得多,是“读多写少”的场景:商品活动详情页访问量巨大,但真正下单扣减库存成功的不多,即查询库存的流量要远远大于实际扣减库存的流量。

在应对这种“秒杀”活动时,需要进行限流操作,例如在提交“抢购”时增加答题的环节或者增加验证码等,利用流量的时间分片来缓冲瞬时大流量。

“秒杀”活动的设计原则

业界总结出了五大设计原则。

(1)数据应尽量少

在用户请求时,发送请求所传输的数据和服务端响应请求所传输的数据应尽可能少。因为,这些数据在网络中传输是需要时间的,消息体太大会影响传输效率。另外,发起请求所传输的数据需要在服务端进行各种解析,如JSON 的反序列化等。这些操作都会消耗CPU 资源,所以减少传输的数据能有效提高 CPU 的使用率。

减少数据的操作包括:

①简化“秒杀”页面的大小,去掉一些不必要的页面装修;

②能预热的数据尽量提前预热;

③在发送“秒杀”请求时,只带上最有用的信息;

④在服务端返回数据时,返回最简数据体。另外,尽量少依赖外部服务,能“自己做”的就“自己做”。

(2)请求数应尽量少。

在用户进入商品“秒杀”详情页时,浏览器会渲染各种额外的数据,如当前页面所关联的CSS、JavaScript 文件、商品图片,以及页面 Ajax发出请求所获取的数据等。这些额外的请求数应尽量少,因为浏览器每发送一次 HTTP请求都会消耗一定的性能,特别是有些请求是串行的(如多个按顺序使用的 JavaScript 文件)。

减少请求数的一个最常用的方案是:将多个静态文件进行合并请求,即将多个CSS 和JavaScript 文件进行合并请求。在请求 URL 中,使用逗号做文件的分割。例如:xxx.com/active/1.js…

这些静态文件在服务端还是独立存放的,只不过在服务端有一个解析此种 URL 的组件(如淘宝开发的 nginx-http-concat),它会将解析出来的各个文件进行合并返回。

(3)请求路径应尽量短。

请求路径可以被理解为“请求从用户端发出到响应数据回到用户端这中间的各个节点”,如浏览器、CDN、负較均衡器、Web 容器等。请求每经过一个节点都需要建立一个 TCP 连接,所以节点离用户越近,则意味着路径越短,这样性能和体验就越好。

缩短请求路径的一般做法是:将依赖性强的应用合并部署在一起,将 RPC 变成对应用内部方法的调用。还可以使用多级缓存来缩短路径。

(4)尽量使用“异步化”。

所谓“异步化”是指,接收端接收用户的所有订单请求,并将这些请求直接丢进队列中;之后下单系统根据自身的实际处理情况从队列中获取订单请求,并将其下发给订单中心生成真正的订单:

同时,系统告知用户当前订单的处理进度,以及他前面有多少人在等待。这么做可以解决“同步化处理不了峰值流量”的问题。

(5) 避免单节点。

单节点是所有系统设计中的大忌,因为单节点系统意味着系统的不稳定性较高,可能会出现不可用的情况,给企业带来直接的损失。在设计系统(特别是“秒杀”系统)时,必须保证系统的高可用。

在设计系统时避免单节点的一个小妙招:将服务无状态化。如果无法完全无状态化(如存储系统),则可以通过冗余多个备份节点来避免单节点。

动静分离方案设计

动静分离是指,将静态页面与动态页面(或者静态数据与动态数据)解耦分离,用不同系统承载对应流量。这样可以提升整个服务的访问性能和可维护性。

在早期的“秒杀”活动中,用户要不停地刷新页面才能获取“秒杀”信息。在如今的“秒杀”活动中,用户提前进入活动详情页后不用做任何的操作,只需等待活动的开始,然后点击“秒杀”或“拾购”接钮即可。这大大提升了用户的参与体验,同时系统的处理速度更快,性能更好。这离不开“动静分离”技术的帮助。

什么是静态数据

静态数据是指页面中几乎不怎么变化的数据(即不依据用户的Cookie、基本信息、地域、感等各种属性来生成的数据),例如:

  • CSS 和 JavaScript 文件中的静态数据。
  • 活动页中的 HTML 静态文件。
  • 图片等相关资源文件。
  • 其他与用户信息无关的静态数据。

在浏览新闻类网站时,所有用户在具体板块中浏览的信息都是一样的(推荐模块除外),其中的数据就是静态数据。

对于这种分离出来的静态数据可以进行缓存。在缓存之后,访问这些静态数据的效率就提高了,系统运行速度也更快了。可以使用代理服务器进行静态数据的缓存,例如:浏览器本地缓存(包括App端、PC端)、CDN、Nginx、Squid、Varnish

什么是动态数据

动态数据是指依据当前用户属性动态生成的数据。

在浏览淘宝首页时,每个用户所看到的商品都是不一样的,这就是淘宝的“千人千面”——针对不同用户做不同的推荐。

在百度搜索中会依据不同用户的输入条件及用户的习惯给出不同的结果页,其中的数据就是动态数据。

处理动态数据主要体现在技术架构上,一般采取如下技术方案:

  • 清晰的分层架构。
  • 服务架构。
  • 缓存架构。

下图为服务与存储(数据库、缓存)的分离

从零设计一个千万级流量的秒杀系统

如何实施动静分离架构

实施动静分离架构可以采用“分而治之”的办法,即将动态数据和静态数据解耦,分别使用各自的架构系统来承载对应的流量:

  • 对于静态数据,建议缩短用户请求路径,因为路径越短,访问速度就越快。另外,应尽能地将静态数据缓存起来。

  • 对于动态数据,一般用户端需要和服务端进行交互才能获取,所以,请求路径越长,访速度就越慢。下图展示了动静分离方案。

从零设计一个千万级流量的秒杀系统

获取静态数据和获取动态数据的域名也不同。

使用“页面静态化”技术实现动静分离架构

访问静态数据的速度很快,而访问动态数据的速度较慢。那么试想一下,可以提前生成好需要动态获取的数据,然后使用静态页面加速技术来访问这些数据吗?如果这样可以,那访问动态数据的速度就变快了。这就需要用到比较流行的“页面静态化”技术。

  1. 什么是“页面静态化”技术。 “页面静态化”技术是指,直接缓存HTTP 连接,而不仅是缓存数据。如图下图所示,代理服务器根据请求的 URL 直接返回 HTTP 对应的响应头及响应消息体,流程简洁且高效。

从零设计一个千万级流量的秒杀系统

例如,在获取商品详情页数据时,可以提前将该商品详情页的动态数据生成好,然后使用静态页面加速技术访问这些数据。如此,系统的整体性能将得到显著提升,且会大大提升用户体验。

  1. “页面静态化”技术适用场景。 “页面静态化”技术这么好—能加速对动态数据的获取,那它是不是在所有场景都适用呢?其实不然,如果使用不当,则不仅不会使系统性能得到提升,反而会使系统性能下降。

    因为“页面静态化”技术需要将动态数据进行提前生成,所以,“页面静态化”技术适用于“动态数据总量不是很大,生成的静态页面数量不多”的业务

    例如:

    • 在“秒杀”活动中,如果“秒杀”商品数量有限,则可以生成数量有限的“秒杀”商品静态网页。

    • 在二手车业务中,如果二手车库存量不是很大,则可以提前生成二手车的静态网页。

    大型博客之类的网站就不适合使用“页面静态化”技术,因为,其中的文章数量是以“亿”为单位的,生成这种体量的静态网页后访问速度会变得更慢

热点数据处理

先来看两个场景。

(1)大型电商网站(如淘宝或者京东)的商品访问量都是以“亿”为单位的,每天都有“干万级别的商品被上亿的用户访问,其中部分商品被很大一部分流量访问和下单,即这部分商品是“热点商品”。 “热点商品”的典型场景是“秒杀”业务场景。在“秒杀”业务中,“热点商品” 在极短的时间内(1s~25)被大量的用户访问,系统将面对流量洪峰的挑战。

(2)微博每天都会产生巨大流量的微博条目,突然某位明星宣布“和某某某正式结婚”,这条微博在发出后会被“干万”级别“粉丝”查看和转发,这条微博可以被定义为“热点微博”。

什么是热点数据

在如上两个场景中,“热点商品”和“热点微博”均为热点数据。在一定时间内被大量用户访问的数据就是热点数据。热点数据又被分为“静态热点数据”和“动态热点数据”。

  • 静态热点数据:可以被提前预知的数据。例如,商家事先决定好了在哪一天要进行“秒杀”活动,或者系统通过历史数据预测出哪个商品容易成为“热点数据”。
  • 动态热点数据:不能被提前预知的数据。例如,大明星宣布的大消息,或者淘宝、京东等平台突然做广告导致的“热点数据”。

如何发现热点数据

  • 静态热点数据的发现比较容易,因为,要么是可以事先定义的热点数据,要么是可以通过历史数据预测出的热点数据。之后,将发现的热点数据写入缓存。
  • 动态热点数据的发现很难。但如果能实时地发现动态热点数据,则对于促进商品的销售将起到很大的作用,同时对于系统也可以起到很好的保护作用。
  • 下面是发现动态热点数据的建议,如图所示。

从零设计一个千万级流量的秒杀系统

(1)使用日志收集组件(如 Flume 组件),实时地收集各个中间件及服务的日志,例如对代理层 Nginx 和后端服务层日志的实时收集。

(2)将实时收集到的日志信息发送到消息中间件 Kafka 中。

(3)构建一套流式计算系统,实时地从 Kafka 中消费日志信息。

(4)流式计算系统对日志信息进行实时流式计算,得到热点数据。

发现动态热,点数据,只要能达到近实时发现(一般为2~3s)即可。

如何处理热点数据

发现热点数据后的一般做法是:优化、限制和隔离。

(1) 热点数据的优化。

直接将发现的热点数据写入分布式缓存,并且根据业务情况对其设置缓存过期时间

(2)热点数据的限制。

热点数据限制是一种保护系统的机制,目的是不让热点数据抢占其他请求的资源。 例如,使用线程池隔离技术将每个热点数据的处理过程放在独立的线程池内,这样可以避免大流量的冲击而影响其他请求使用机器资源。

(3)热点数据的隔离。

热点数据的隔离也是一种保护机制:不能因为数据访问量过大而造成整个业务的崩溃。 例如,针对“秒杀”场景,可以先将“秒杀”业务和其他主线业务隔离开,防止它们互相影响;然后,将“秒杀”应用独立部署,且使用独立的域名;最后,“秒杀”系统使用独立的数据库进行存储。

大流量的高效管控

在“秒杀”场景中,海量的用户会带来瞬时的流量洪峰。瞬时的流量洪峰会带来严重的服务端读/写性能问题、数据库锁,以及服务器资源占用等问题。

在“秒杀”业务中,商品价格具有强大的吸引力,所以会受到很多用户的关注,但是商品数量是有限的。

所以,在千万个用户中可能只有100人能得到商品,对于系统来说,90%以上的流量属于无效流量。

“秒杀”业务希望有大量用户关注“秒杀”活动,但是在用户真正下单时又不能将这些流量全部放过,所以,需要设计一套高效的流量管控方案,来有效地控制请求流量,过滤掉没必要的流量。

流量分层

通过前面对动静分离方案及热点数据处理的学习,如果将流量都静态缓存了,那是不是就可以很好地保护后端系统呢?

请求路径越短,则访问速度肯定越快。但是,业务特性决定了不能永远将数据放在静态缓存中,接下来看一下如何对瞬时流量洪峰进行分层控制。

如图2所示,对于瞬时流量洪峰采用倒三角的分层级逐层控制方式,共分为CDN、反向代理(Nginx)、后端服务及 DB这四个层级。接下来,就来看看每一层级是怎么控制流量的。

从零设计一个千万级流量的秒杀系统

在部分“秒杀”业务场景中,用户的浏览器也可以进行一级数据缓存:浏览器是最接近用户的,对于时效很长且体积不大的静态数据,可以将其放入浏览器本地缓存中。

流量分层控制

接下来看看每一层流量该如何进行控制。

(1) CDN 层流量控制。 由动静分离技术可以想到:应提前生成尽可能多的数据,然后将其放入CDN 节点缓存中(因为 CDN 层在物理架构上离用户比较近)。

所以,如果绝大部分的流量都在这一层获取数据,那到达后端的流量就会减少很多

(2)反向代理层流量控制。

在动静分离方案中,讲到通过“页面静态化”技术可以加速动态数据的获取,即提前将动态数据生成好,然后对其进行静态化处理。

所以,这里就可以依据“页面静态化”技术,通过后端服务 Job 的方式定时提前生成好前端需要的静态数据,然后,将其发送到内容分发服务上,内容分发

务会将这些静态数据分发给所有的反向代理服务器,如图所示。

从零设计一个千万级流量的秒杀系统

分发的这些静态数据,可以根据具体业务场景需要进行定时更新,例如“秒杀”详情页面的商品属性信息更新。除分发外,还可以利用 Nginx的缓存配置功能配置后端接口获取热点数据进行缓存。

  • 在“秒杀”业务中,活动详情页上有一个倒计时的模块,用户可以看到当前“秒杀”活动还剩余多少时间开始。这种逻辑简单的功能可以直接使用 Nginx 来实现:利用 nginx-lua 插件,使用Lua 脚本获取当前 Nginx服务器的时间来计算倒计时。另外,商品库存数据也可以通过Nginx直接访问分布式缓存来获取
  • 在“秒杀”业务中,可能会有人利用“秒杀器”进行不公平竞争,且有可能存在竞争对手恶着刷请求的情况。如果存在这样的情况,那本次活动就是有风险的,万一被恶意流量独占了库存,则会导致正常用户不能抢购商品,也有可能这种恶意请求会对后端系统造成严重冲击,甚至造成后章系统瘫痪。
  • 对于这种恶意请求,最好有一套机制能提前感知,并将恶意请求提前封存。可以在Nginx层中控制;也可以在Nginx 中配置用户的访问频率(例如每分钟最多只能访问10次);还可以使兵Lua 脚本编写一些简单业务逻辑的接口,例如,通过调用接口直接封掉指定IP地址多或UserAgent.

可以利用大数据的日志收集组件(如 Flume)从 Nginx 上采集日志,将采集到的日志写入存储系统(如HBase)中,然后风控平台对存储系统中的日志进行风险分析。对于有风险的请求,风控平台可以直接调用 Nginx 中的Lua风控接口对其进行封停处理,例如,禁止某个 IP地址或将请求的 UserAgent 封停,如图所示。

从零设计一个千万级流量的秒杀系统 (3)后端服务层流量控制。

对于服务层的流量控制,有以下几点建议:

  • 在程序开发上,代码独立,不要与平台其他项目合在一起。

  • 在部署时,应用独立部署,分散流量,避免不合适的流量影响主体业务。

  • 使用独立域名,或者按照一定的 URL 规则在反向代理层进行路由。

  • 做好系统保护和限流,进一步减少不必要的流量。

    当“到达系统中的请求数”明显大于“系统能够处理的最大请求数”时,可以直接拒绝多余的请求,直接返回“秒杀”活动结束的信息。

(4)数据库层流量控制。

写数据库的流量就是真正下单成功的流量,即需要扣减库存的动作。有如下建议:

  • 如果不是临时的活动,则建议使用独立的数据库作为“秒杀”活动的数据库。
  • 将数据库配置成读写分离。
  • 尝试去除行锁。

对于教据库行锁的优化,可以通过将商品进行拆分来实现—增加1,如下表所示。对于单一的“秒杀”活动这会得到显著效果。

商品 ID商品名称数量
1000000Meta 403
分配 ID商品ID分配标识
1000000A0110000001
1000000A0210000001
1000000A0310000001

从流量分层控制方案可以看出,瞬时流量就像被漏斗过滤了似的,应尽量将数据和请求量一品层地过滤掉。这种流量分层控制的核心思想是:在不同的层级中尽可能地过滤掉无效的请求,到送“倒三角”最末端的请求才是有效的请求。

扣減库存的那些事

在电商网站中,购买流程一般是这样的:在商品列表页看中一个商品,然后进入其详情页浏览详细信息,之后单击“立即购买”按钮(或者将商品加入购物车,从购物车进行结算),接着,网站弹出支付方式让用户进行支付。

对于这样的购买流程,用户在下单时一般是不需要关心库存的,只有没有库存了用户才会感到没有库存了。但是在高并发场景中,这样的购买流程会出现问题。例如,在“秒杀”活动中用户会争抢库存,如果没有控制好则会出现“超卖”的情况。

对于“秒杀”活动,一般公司是不允许商品“超卖”(即下单成功的数量不能大于商品存库数量)的。一旦“超卖”,则会给公司造成损失。如果被恶意流量利用,则损失是巨大的。

  1. 扣减库存方式 库存对于电商平台来说是一个重要的业务指标,所以在技术上需要合理设计扣减库存,不能现“超卖”的情况。

    扣减库存常有以下3种方式

  • 下单后扣减库存:在用户下单后就扣减库存,如图所示。这种扣减方式最简单,也最好理解。但问题是,可能有用户下单后不付款,特别是竞争对手利用“秒杀器”大量抢购商品,但是不支付。如果是这样,那商家就没有达到活动的真正目的,且真正想得到商品的用户也无法得到低价商品。

从零设计一个千万级流量的秒杀系统

  • 支付后扣减库存:在用户付完款后再扣减库存,这样可以解决用户“下单但不支付”的情况。但这种方式也有问题——会发生“超卖”。因为,在瞬时洪峰流量下,会有很多用户付款成功,但只有一小部分用户能真正抢到商品,大部分用户在付完款后系统会报“己售罄”或“活动结束”等,如图

从零设计一个千万级流量的秒杀系统

对于“支付后扣减库存”这种方式,当出现库存不足时,有些商家是可以后续再补货的。

  • 预扣减库存:在用户下完订单后,系统会为其锁定库存一段时间,例如 30分钟;在超过锁定时间后会自动释放锁定的库存,让其他用户抢购。当用户付款时,系统会校验库存是否在锁定有效期内,如果在有效期内,则可以进行支付;如果锁定有效期已过,则重新锁定库存,若锁定失败则报“库存不足”的提醒

在用户下单后,有些平台会有一个支付时间的倒计时。例如12306 网站,会有一个30分钟的有效支付时间,这种扣减库存方式相当于“预扣减库存”方式。 但这种方式也存在恶意用户占用有效锁定时间的可能。

  1. 在千万级流量的“秒杀”中,如何扣减库存

如上已经知道了扣减库存方式共有三种:下单后扣减库存、支付后扣减库存及预扣减库存。对于“秒杀”业务,该如何选择扣减库存的方式呢?

由于在“秒杀”场景中商品一般对用户很具有吸引力,所以,在这种场景中使用“下单后扣减库存”方式更为合适。

在“秒杀”场景中,大部分用户抱着“抢到就是赚到”的想法,基本都会付款,但如果真有竞争对手恶意下单不付款,那我们该怎么办?

前面在流量管控中已经说到,可以对请求日志进行实时分析,让风控系统选择出恶意用户,然后将其封停。对于“下单后扣减库存”这种方式,利用数据库的事务特性,可以保证订单和库存扣减数量的一致性。

下面来分析该如何去做好“下单后扣减库存”,防止商品被“超卖”。

(1)将某个商品的库存数量查询出来。例如,

Select num form stock where pro_id - Sproid

(2)更新库存。用“库存数量”减去“购买的商品数量”,然后将结果值更新到库存数据库中。

假如,商品库存数量为 10,之后用户购买了2件,则得到的结果值是(10-2=8),将8更新为最新的库存数量。

-- 计算 10 - 2 = 8
-- 结果集
-- 更新库存
update stock set num = 8 where pro_id = $proId

为什么是更新“结果值”的操作,而不是直接更新“扣减”的操作呢?

因为,“扣减”不是幂等的,如果接口设计得不够完善,没有考虑到幂等性,那么在由于网络原因或者其他原因造成重试后,会出现重复“扣减”,即会出现“超卖”,甚至库存为负值的情况。

通过上面两步的处理,基本可以保证下单扣减库存的准确性,但对于“秒杀”这样的高并发场景来说这样还是有风险的。例如,在大流量、高并发下,有两个用户同时抢购,都拿到了库存数量为10 的商品,其中一个用户购买了5 件商品,随后更新库存数量为(10-5=5)件;接着另一个请求购买了3件商品,随后更新库存数量为(10-3=7)件,如图所示。

从零设计一个千万级流量的秒杀系统

在高并发场景中,有可能出现并发更新数据不一致的情况。对于图中的两次请求,正常来讲应该卖出8件商品,但现在却卖出了3 件商品,因为 update set 动作覆盖了之前的数据。

在更新库存数量时,需要将“当前的库存数量”与“之前的库存数量”进行比对,如下所示:

Update stock set num = new_num where pro_id = prold and num = old_num

有了这种比对,在并发更新时,用户A和用户B只有在更新提交前查询到的库存为 10,才能更新库存成功。

但是,在这种并发场景中,如果没有进行比对,则会出现以下问题:

  • 开始时库存数量为10,第一个用户更新库存数量是成功的。
  • 在第一个用户更新完库存数量后,库存数量变成了 5,所以,这时另一个请求是不能更新成功的。

在干万级流量的 “秒杀”中,优化库存数量扣减

在“秒杀”场景中,通过流量分层控制可以分层管控大量的“读”请求。但是,依然会有很多流量进入真正的下单逻辑。对于这么大的流量,除前面说的数据库隔离外,还需要进一步优化度存数量,否则数据库读/写依然是系统的瓶颈。

接下来看看如何优化大流量“秒杀”场景中的库存数量扣減操作。

(1)利用好缓存。 在“秒杀”场景中,如果只是一个扣减库存数量这样的简单流程,则可以先将库存数量直接放在缓存中,然后利用分布式缓存(如 Redis)去应对这种瞬时流量洪峰下的系统挑战。

使用缓存是存在一定风险的,比如,缓存节点出现了异常,那库存数量该怎么算?

使用缓存不仅要考虑分布式缓存高可用,还要考虑各种限流容错机制,以确保分布式缓存对外提供服务。

(2)异步处理。 如果是复杂的扣减库存(如涉及商品信息本身或牵连其他系统),则建议使用数据库进行库存数量的扣减,可以使用异步的方式来应对这种高并发的库存数量更新。

  • 在用户下单时,不立刻生成订单,而是将所有订单依次放入队列。
  • 下单模块依据自身的处理速度,从队列中依次获取订单进行“下单扣减库存”操作。
  • 在订单生成成功后,用户即可进行支付操作了。

这种方式是针对“秒杀”场景的,依据“先到先得”的原则来保证公平公正,所有用户都可以抢购,然后等待订单处理,最后生成订单(如果库存不足,则生成订单失败)。这样的逻辑,对户来说体验感不是很差。具体排队逻辑如图所示。

从零设计一个千万级流量的秒杀系统

对于用户来说,只需在商品活动详情页中提交一次抢购请求,之后就是等待系统处理进度。当然,这个等待时间是要设计的,不要让用户等太长时间,不管成功或失败。

文章借阅书本《高并发系统实战派》,著作谢恩德,看完收获颇多,推荐大家购买学习