整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

这一次,彻底弄懂“秒杀系统”

51CTO.com原创稿件】说到“秒杀”,恐怕大多数人想到的就是“双 11”,“促销”,“买买买”等火爆的场面吧。



图片来自 Pexels

大家为了打折商品蜂拥而至,造成电商网站一片繁华的景象。但作为程序员的我们,看到的却是背后的高并发和可靠性。无论你处在软件开发的哪个阶段,都希望能够设计一套属于自己的秒杀系统。

今天我们一起来看看,一套秒杀系统在架构设计上需要有哪些考量:

  • 秒杀场景的特点
  • 系统隔离的设计思路
  • 客户端设计
  • 代理层设计
  • 应用层设计
  • 数据库设计
  • 压力测试
  • 总结

秒杀场景的特点

秒杀场景是电商网站定期举办的活动,这个活动有明确的开始和结束时间,而且参与互动的商品是事先定义好了,参与秒杀商品的个数也是有限制的。同时会提供一个秒杀的入口,让用户通过这个入口进行抢购。

总结一下秒杀场景的特点:

  • 定时开始,秒杀时大量用户会在同一时间,抢购同一商品,网站瞬时流量激增。
  • 库存有限,秒杀下单数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 操作可靠,秒杀业务流程比较简单,一般就是下订单减库存。库存就是用户争夺的“资源”,实际被消费的“资源”不能超过计划要售出的“资源”,也就是不能被“超卖”。

系统隔离的设计思路

在分析秒杀的特点后,我们发现秒杀活动是有计划的,并且在短时间内会爆发大量的请求。为了不影响现有的业务系统的正常运行,我们需要把它和现有的系统做隔离。

即使秒杀活动出现问题也不会影响现有的系统。隔离的设计思路可以从三个维度来思考。

  • 业务隔离
  • 技术隔离
  • 数据库隔离

业务隔离

既然秒杀是一场活动,那它一定和常规的业务不同,我们可以把它当成一个单独的项目来看。在活动开始之前,最好设计一个“热场”。

“热场”的形式多种多样,例如:分享活动领优惠券,领秒杀名额等等。“热场”的形式不重要,重要的是通过它获取一些准备信息。

例如:有可能参与的用户数,他们的地域分布,他们感兴趣的商品。为后面的技术架构提供数据支持。

技术隔离



技术隔离架构图

前面有了准备工作,那么从技术上需要有以下几个方面的考虑:

  • 客户端,前端秒杀页面使用专门的页面,这些页面包括静态的 HTML 和动态的 JS,他们都需要在 CDN 上缓存。
  • 接入层,加入过滤器专门处理秒杀请求,即使我们扩展再多的应用,使用再多的应用服务器,部署再多的负载均衡器,都会遇到支撑不住海量请求的时候。

所以,在这一层我们要考虑的是如何做好限流,当超过系统承受范围的时候,需要果断阻止请求的涌入。

  • 应用层,瞬时的海量请求好比请求的“高峰”,我们架构系统的目的就是“削峰”。

需要使用服务集群和水平扩展,让“高峰”请求分流到不同的服务器进行处理。同时,还会利用缓存和队列技术减轻应用处理的压力,通过异步请求的方式做到最终一致性。

由于是多线程操作,而且商品的额度有限,为了解决超卖的问题,需要考虑进程锁的问题。

数据库隔离

秒杀活动持续时间短,瞬时数据量大。为了不影响现有数据库的正常业务,可以建立新的库或者表来处理。

在秒杀结束以后,需要把这部分数据同步到主业务系统中,或者查询表中。如果数据量特别巨大,到千万级别甚至上亿,建议使用分表或者分库。

客户端设计

上面提到的三个隔离维度中,我们对技术维度是最为关心的。如果说浏览器/客户端是用户接触“秒杀系统”的入口,那么在这一层提供缓存数据就是非常必要的。

在设计之初,我们会为秒杀的商品生成专门的商品页面和订单页面。这些页面以静态的 HTML 为主,包括的动态信息尽量少。

从业务的角度来说,这些商品的信息早就被用户熟识了,在秒杀的时候,他们关心的是如何快速下单。

既然商品的详情页面和订单页面都是静态生成的,那么就需要定义一个 URL,当要开始秒杀之前,开放这个 URL 给用户访问。

为了防止“程序员或者内部人员”作弊,这里的地址可以通过时间戳和 Hash 算法来生成,也就是说这个地址只有系统知道,到了快秒杀之前才由系统发放出去。

有人说浏览器/客户端如果存放的都是静态页面,那么“控制开始下单”的按钮,以及发送“下单请求”的按钮,也是静态的吗?

答案是否定的,其实静态页面是方便客户端好缓存,下单的动作以及下单时间的控制还是在服务器端。

只不过是通过 JS 文件的方式发送给客户端,在快要秒杀之前,会把这部分 JS 下载到客户端。

因为,其业务逻辑很少,基本只包括时间,用户信息,商品信息等等。所以,其对网络的要求不高。

同时,在网络设计上,我们也会将 JS 和 HTML 同时缓存在 CDN 上面,让用户从离自己最近的 CDN 服务器上获取这些信息。

为了避免秒杀程序参与秒杀,在客户端也会设计一些问答或者滑块的功能,减少此类机器人对服务器的压力。



秒杀系统前端设计简图

代理层设计

说完了秒杀系统的前端设计,请求自然地来到了代理层。由于用户的请求量大,我们需要用负载均衡加上服务器集群,来面对如此空前的压力。



代理层三大功能简图

在这一层是可以做缓存,过滤和限流的:

  • 缓存,以 Nginx 为例,它可以缓存用户的信息。假设用户信息的修改没有那么频繁,即使有类似的修改也可以通过更新服务来刷新。总比从服务器上获取效率要高得多。
  • 过滤,既然缓存了用户信息,这里就可以过滤掉一些不满足条件的用户。注意,这里的用户信息的过滤和缓存只是一个例子。

主要想表达的意思是,可以将一些变化不频繁的数据,提到代理层来缓存,提高响应的效率。

同时,还可以根据风控系统返回的信息,过滤一些疑似机器人或者恶意请求。例如:从固定 IP 过来的,频率过高的请求。最重要的就是在这一层,可以识别来自秒杀系统的请求。

如果是带有秒杀系统的参数,就要把请求路由到秒杀系统的服务器集群。这样才能和正常的业务系统分割开来。

  • 限流,每个服务器集群能够承受的压力都是有限的。代理层可以根据服务器集群能够承受的最大压力,设置流量的阀值。

阀值的设置可以是动态调整的。例如:集群服务器中有 10 个服务器,其中一台由于压力过大挂掉了。

此时就需要调整代理层的流量阀值,将能够处理的请求流量减少,保护后端的应用服务器。

当服务器恢复以后,又可以将阀值调回原位。可以通过 Nginx+Lua 合作完成,Lua 从服务注册中心读取服务健康状态,动态调整流量。

应用层设计

“秒杀系统”秒杀的是什么?无非是商品。对于系统来说就是商品的库存,购买的商品一旦超过了库存就不能再卖了。

防止超卖

超过了库存还可以卖给用户,这就是“超卖”,也是系统设计需要避免的。为了承受大流量的访问,我们用了水平扩展的服务,但是对于他们消费的资源“库存”来说,却只有一个。

为了提高效率,会将这个库存信息放到缓存中。以流行的 Redis 为例,用它存放库存信息,由多个线程来访问就会出现资源争夺的情况。也就是分布式程序争夺唯一资源,为了解决这个问题我们需要实现分布式锁。

假设这里有多个应用响应用户的订单请求,他们同时会去访问 Redis 中存放的库存信息,每接受用户一次请求,都会从 Redis 的库存中减去 1 个商品库存量。

当任何一个进程访问 Redis 中的库存资源时,其他进程是不能访问的,所以这里需要考虑锁的情况(乐观,悲观)。



Redis 缓存承载库存变量

如果锁长期没有释放,需要考虑锁的过期时间,需要设置两个超时时间:

  • 资源本身的超时时间,一旦资源被使用一段时间还没有被释放,Redis 会自动释放掉该资源给其他服务使用。
  • 服务获取资源的超时时间,一旦一个服务获取资源一段时间后,不管该服务是否处理完这个资源,都需要释放该资源给其他服务使用。

订单处理流程

这里的“扣减服务”完成了最简单的扣减库存工作,并没有和其他项目服务打交道,更没有访问数据库。



订单流程示意图

后面的流程相对比较复杂,我们先看图,根据图示来讲解:

  • 首先,扣减服务作为下单流程的入口,会先对商品的库存做扣减。同样它会检查商品是否还有库存?
  • 由于订单对应的操作步骤比较多,为了让流量变得平滑,这里使用队列存放每个订单请求,等待订单处理服务完成具体业务。
  • 订单处理服务实现多线程,或者水平扩展的服务阵列,它们不断监听队列中的消息。一旦发现有新订单请求,就取出订单进行后续处理。

注意,这里可以加入类似 ZooKeeper 这样的服务调度来帮助,协调服务调度和任务分配。

  • 订单处理服务,处理完订单以后会把结果写到数据库。写数据库是 IO 操作,耗时长。
  • 所以,在写数据库的同时,会把结果先写入缓存中,这样用户是可以第一时间查询自己是否下单成功了。
  • 结果写入数据库,这个操作有可能成功也有可能失败。
  • 为了保证数据的最终一致性,我们用订单结果同步的服务不断的对比,缓存和数据库中的订单结果信息。

一旦发现不一致,会去做重试操作。如果重试依旧不成功,会重写信息到缓存,让用户知道失败原因。

  • 用户下单以后,焦虑地刷新页面查看下单的结果,实际上是读到的缓存上的下单结果信息。

虽然,这个信息和最终结果有偏差,但是在秒杀的场景,要求高性能是前提,结果的一致性,可以后期补偿。

数据库设计

讲完了秒杀的处理流程,来谈谈数据库设计要注意的点。

数据估算

前面说了秒杀场景需要注意隔离,这里的隔离包括“业务隔离”。就是说我们在秒杀之前,需要通过业务的手段,例如:热场活动,问卷调查,历史数据分析。通过他们去估算这次秒杀可能需要存储的数据量。

这里有两部分的数据需要考虑:

  • 业务数据
  • 日志数据

前者不言而喻是给业务系统用的。后者,是用来分析和后续处理问题订单用的,秒杀完毕以后还可以用来复盘。

分表分库

对于这些数据的存放,需要分情况讨论,例如,MySQL 单表推荐的存储量是 500W 条记录(经验数字)。

如果估算的时候超过了这个数据,建议做分表。如果服务的连接数较多,建议进行分库的操作。

数据隔离

由于大量的数据操作是插入,有少部分的修改操作。如果使用关系型数据来存储,建议用专门的表来存放,不建议使用业务系统正在使用的表。

这个开头提到了,数据隔离是必须的,一旦秒杀系统挂了,不会影响到正常业务系统,这个风险意识要有。表的设计除了 ID 以外,最好不要设置其他主键,保证能够快速地插入。

数据合并

由于是用的专用表存储,在秒杀活动完毕以后,需要将其和现有的数据做合并。其实,交易已经完成,合并的目的也就是查询。

这个合并需要根据具体情况来分析,如果对于那些“只读”的数据,对于做了读写分离的公司,可以导入到专门负责读的数据库或者 NoSQL 数据库中。

压力测试

构建了秒杀系统,一定会面临上线,那么在上线之前压力测试是必不可少的。

我们做压力测试的目的是检验系统崩溃的边缘在哪里?系统的极限在哪里?

这样才能合理地设置流量的上限,为了保证系统的稳定性,多余的流量需要被抛弃。

压力测试的方法

合理的测试方法可以帮助我们对系统有深入的了解,这里介绍两种压力测试的方法:

  • 正压力测试
  • 负压力测试

正压力测试。每次秒杀活动都会计划,使用多少服务器资源,承受多少的请求量。

可以在这个请求量上面不断加压,直到系统接近崩溃或者真正崩溃。简单的说就是做加法。



正压力测试示意图

负压力测试。在系统正常运行的情况下,逐步减少支撑系统的资源(服务器),看什么时候系统无法支撑正常的业务请求。

例如:在系统正常运行的情况下,逐步减少服务器或者微服务的数量,观察业务请求的情况。说白了就是做减法。



负压力测试示意图

压力测试的步骤



测试步骤

有了测试方法的加持,我们来看看需要遵循哪些测试步骤。下面的操作偏套路化,大家在其他系统的压力测试也可以这么做,给大家做个参考。

第一,确定测试目标。与性能测试不同的是,压力测试的目标是,什么时候系统会接近崩溃。比如:需要支撑 500W 访问量。

第二,确定关键功能。压力测试其实是有重点的,根据 2/8 原则,系统中 20% 的功能被使用的是最多的,我们可以针对这些核心功能进行压力测试。例如:下单,库存扣减。



关注核心服务

第三,确定负载。这个和关键服务的思路一致,不是每个服务都有高负载的,我们的测试其实是要关注那些负载量大的服务,或者是一段时间内系统中某些服务的负载有波动。这些都是测试目标。

第四,选择环境,建议搭建和生产环境一模一样的环境进行测试。

第五,确定监视点,实际上就是对关注的参数进行监视,例如 CPU 负载,内存使用率,系统吞吐量等等。

第六,产生负载,这里需要从生产环境去获取一些真实的数据作为负载数据源,这部分数据源根据目标系统的承受要求由脚本驱动,对系统进行冲击。

建议使用往期秒杀系统的数据,或者实际生产系统的数据进行测试。

第七,执行测试,这里主要是根据目标系统,关键组件,用负载进行测试,返回监视点的数据。

建议团队可以对测试定一个计划,模拟不同的网络环境,硬件条件进行有规律的测试。

第八,分析数据,针对测试的目的,对关键服务的压力测试数据进行分析得知该服务的承受上限在哪里。

对一段时间内有负载波动或者大负载的服务进行数据分析,得出服务改造的方向。

总结

秒杀系统的特点,并发量大,资源有限,操作相对简单,访问的都是热点数据。因此,我们需要把它从业务,技术,数据上做隔离,保证不影响到现有的系统。

因此,架构设计需要分几层来考虑,从客户请求到数据库存储,到最后上线前的压力测试。



简易的思维导图送给大家

思考顺序如下,客户端→代理层→应用层→数据库→压力测试:

客户端 90% 静态 HTML+10% 动态 JS;配合 CDN 做好缓存工作。

接入层专注于过滤和限流。

应用层利用缓存+队列+分布式处理好订单。

做好数据的预估,隔离,合并。

上线之前记得进行压力测试。

作者:崔皓

简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

荐阅读:

  1. 学会这些微服务+Tomcat+NGINX+MySQL+Redis,再去面试阿里P7岗吧
  2. “火爆”的微服务架构你还不会?从基础到原理的PDF文档快来学!
  3. Nginx负载均衡+Tomcat架构还不透彻?Java架构师必读书籍送给你

前言

很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段,不知道学习的并发知识如何运用到实际项目中,就更别提如何构建高并发系统了!

究竟什么样的系统算是高并发系统?今天,我们就一起解密高并发业务场景下典型的秒杀系统的架构,结合高并发专题下的其他文章,学以致用。

电商系统架构

在电商领域,存在着典型的秒杀业务场景,那何谓秒杀场景呢。简单的来说就是一件商品的购买人数远远大于这件商品的库存,而且这件商品在很短的时间内就会被抢购一空。 比如每年的618、双11大促,小米新品促销等业务场景,就是典型的秒杀业务场景。

我们可以将电商系统的架构简化成下图所示。

由图所示,我们可以简单的将电商系统的核心层分为:负载均衡层、应用层和持久层。接下来,我们就预估下每一层的并发量。

  • 假如负载均衡层使用的是高性能的Nginx,则我们可以预估Nginx最大的并发度为:10W+,这里是以万为单位。
  • 假设应用层我们使用的是Tomcat,而Tomcat的最大并发度可以预估为800左右,这里是以百为单位。
  • 假设持久层的缓存使用的是Redis,数据库使用的是MySQL,MySQL的最大并发度可以预估为1000左右,以千为单位。Redis的最大并发度可以预估为5W左右,以万为单位。

所以,负载均衡层、应用层和持久层各自的并发度是不同的,那么,为了提升系统的总体并发度和缓存,我们通常可以采取哪些方案呢?

(1)系统扩容

系统扩容包括垂直扩容和水平扩容,增加设备和机器配置,绝大多数的场景有效。

(2)缓存

本地缓存或者集中式缓存,减少网络IO,基于内存读取数据。大部分场景有效。

(3)读写分离

采用读写分离,分而治之,增加机器的并行处理能力。

秒杀系统的特点

对于秒杀系统来说,我们可以从业务和技术两个角度来阐述其自身存在的一些特点。

秒杀系统的业务特点

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

再比如,小米秒杀系统,在上午10点开售商品,10点前的访问量比较平缓,10点时同样会出现并发量瞬时突增的现象。

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

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

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

(1)限时、限量、限价

在规定的时间内进行;秒杀活动中商品的数量有限;商品的价格会远远低于原来的价格,也就是说,在秒杀活动中,商品会以远远低于原来的价格出售。

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

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

(2)活动预热

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

(3)持续时间短

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

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

秒杀系统的技术特点

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

(1)瞬时并发量非常高

大量用户会在同一时间抢购商品;瞬间并发峰值非常高。

(2)读多写少

系统中商品页的访问量巨大;商品的可购买数量非常少;库存的查询访问数量远远大于商品的购买数量。

在商品页中往往会加入一些限流措施,例如早期的秒杀系统商品页会加入验证码来平滑前端对系统的访问流量,近期的秒杀系统商品详情页会在用户打开页面时,提示用户登录系统。这都是对系统的访问进行限流的一些措施。

(3)流程简单

秒杀系统的业务流程一般比较简单;总体上来说,秒杀系统的业务流程可以概括为:下单减库存。

针对这种短时间内大流量的系统来说,就不太适合使用系统扩容了,因为即使系统扩容了,也就是在很短的时间内会使用到扩容后的系统,大部分时间内,系统无需扩容即可正常访问。 那么,我们可以采取哪些方案来提升系统的秒杀性能呢?

秒杀系统方案

针对秒杀系统的特点,我们可以采取如下的措施来提升系统的性能。

(1)异步解耦

将整体流程进行拆解,核心流程通过队列方式进行控制。

(2)限流防刷

控制网站整体流量,提高请求的门槛,避免系统资源耗尽。

(3)资源控制

将整体流程中的资源调度进行控制,扬长避短。

由于应用层能够承载的并发量比缓存的并发量少很多。所以,在高并发系统中,我们可以直接使用OpenResty由负载均衡层访问缓存,避免了调用应用层的性能损耗。大家可以到https://openresty.org/cn/来了解有关OpenResty更多的知识。同时,由于秒杀系统中,商品数量比较少,我们也可以使用动态渲染技术,CDN技术来加速网站的访问性能。

如果在秒杀活动开始时,并发量太高时,我们可以将用户的请求放入队列中进行处理,并为用户弹出排队页面。

秒杀系统时序图

网上很多的秒杀系统和对秒杀系统的解决方案,并不是真正的秒杀系统,他们采用的只是同步处理请求的方案,一旦并发量真的上来了,他们所谓的秒杀系统的性能会急剧下降。我们先来看一下秒杀系统在同步下单时的时序图。

同步下单流程

1.用户发起秒杀请求

在同步下单流程中,首先,用户发起秒杀请求。商城服务需要依次执行如下流程来处理秒杀请求的业务。

(1)识别验证码是否正确

商城服务判断用户发起秒杀请求时提交的验证码是否正确。

(2)判断活动是否已经结束

验证当前秒杀活动是否已经结束。

(3)验证访问请求是否处于黑名单

在电商领域中,存在着很多的恶意竞争,也就是说,其他商家可能会通过不正当手段来恶意请求秒杀系统,占用系统大量的带宽和其他系统资源。此时,就需要使用风控系统等实现黑名单机制。为了简单,也可以使用拦截器统计访问频次实现黑名单机制。

(4)验证真实库存是否足够

系统需要验证商品的真实库存是否足够,是否能够支持本次秒杀活动的商品库存量。

(5)扣减缓存中的库存

在秒杀业务中,往往会将商品库存等信息存放在缓存中,此时,还需要验证秒杀活动使用的商品库存是否足够,并且需要扣减秒杀活动的商品库存数量。

(6)计算秒杀的价格

由于在秒杀活动中,商品的秒杀价格和商品的真实价格存在差异,所以,需要计算商品的秒杀价格。

注意:如果在秒杀场景中,系统涉及的业务更加复杂的话,会涉及更多的业务操作,这里,我只是列举出一些常见的业务操作。

2.提交订单

(1)订单入口

将用户提交的订单信息保存到数据库中。

(2)扣减真实库存

订单入库后,需要在商品的真实库存中将本次成功下单的商品数量扣除。

如果我们使用上述流程开发了一个秒杀系统,当用户发起秒杀请求时,由于系统每个业务流程都是串行执行的,整体上系统的性能不会太高,当并发量太高时,我们会为用户弹出下面的排队页面,来提示用户进行等待。

此时的排队时间可能是15秒,也可能是30秒,甚至是更长时间。这就存在一个问题:在用户发起秒杀请求到服务器返回结果的这段时间内,客户端和服务器之间的连接不会被释放,这就会占大量占用服务器的资源。

网上很多介绍如何实现秒杀系统的文章都是采用的这种方式,那么,这种方式能做秒杀系统吗?答案是可以做,但是这种方式支撑的并发量并不是太高。此时,有些网友可能会问:我们公司就是这样做的秒杀系统啊!上线后一直在用,没啥问题啊!我想说的是:使用同步下单方式确实可以做秒杀系统,但是同步下单的性能不会太高。之所以你们公司采用同步下单的方式做秒杀系统没出现大的问题,那是因为你们的秒杀系统的并发量没达到一定的量级,也就是说,你们的秒杀系统的并发量其实并不高。

所以,很多所谓的秒杀系统,存在着秒杀的业务,但是称不上真正的秒杀系统,原因就在于他们使用的是同步的下单流程,限制了系统的并发流量。之所以上线后没出现太大的问题,是因为系统的并发量不高,不足以压死整个系统。

如果12306、淘宝、天猫、京东、小米等大型商城的秒杀系统是这么玩的话,那么,他们的系统迟早会被玩死,他们的系统工程师不被开除才怪!所以,在秒杀系统中,这种同步处理下单的业务流程的方案是不可取的。

以上就是同步下单的整个流程操作,如果下单流程更加复杂的话,就会涉及到更多的业务操作。

异步下单流程

既然同步下单流程的秒杀系统称不上真正的秒杀系统,那我们就需要采用异步的下单流程了。异步的下单流程不会限制系统的高并发流量。

1.用户发起秒杀请求

用户发起秒杀请求后,商城服务会经过如下业务流程。

(1)检测验证码是否正确

用户发起秒杀请求时,会将验证码一同发送过来,系统会检验验证码是否有效,并且是否正确。

(2)是否限流

系统会对用户的请求进行是否限流的判断,这里,我们可以通过判断消息队列的长度来进行判断。因为我们将用户的请求放在了消息队列中,消息队列中堆积的是用户的请求,我们可以根据当前消息队列中存在的待处理的请求数量来判断是否需要对用户的请求进行限流处理。

例如,在秒杀活动中,我们出售1000件商品,此时在消息队列中存在1000个请求,如果后续仍然有用户发起秒杀请求,则后续的请求我们可以不再处理,直接向用户返回商品已售完的提示。

所以,使用限流后,我们可以更快的处理用户的请求和释放连接的资源。

(3)发送MQ

用户的秒杀请求通过前面的验证后,我们就可以将用户的请求参数等信息发送到MQ中进行异步处理,同时,向用户响应结果信息。在商城服务中,会有专门的异步任务处理模块来消费消息队列中的请求,并处理后续的异步流程。

在用户发起秒杀请求时,异步下单流程比同步下单流程处理的业务操作更少,它将后续的操作通过MQ发送给异步处理模块进行处理,并迅速向用户返回响应结果,释放请求连接。

2.异步处理

我们可以将下单流程的如下操作进行异步处理。

(1)判断活动是否已经结束

(2)判断本次请求是否处于系统黑名单,为了防止电商领域同行的恶意竞争可以为系统增加黑名单机制,将恶意的请求放入系统的黑名单中。可以使用拦截器统计访问频次来实现。

(3)扣减缓存中的秒杀商品的库存数量。

(4)生成秒杀Token,这个Token是绑定当前用户和当前秒杀活动的,只有生成了秒杀Token的请求才有资格进行秒杀活动。

这里我们引入了异步处理机制,在异步处理中,系统使用多少资源,分配多少线程来处理相应的任务,是可以进行控制的。

3.短轮询查询秒杀结果

这里,可以采取客户端短轮询查询是否获得秒杀资格的方案。例如,客户端可以每隔3秒钟轮询请求服务器,查询是否获得秒杀资格,这里,我们在服务器的处理就是判断当前用户是否存在秒杀Token,如果服务器为当前用户生成了秒杀Token,则当前用户存在秒杀资格。否则继续轮询查询,直到超时或者服务器返回商品已售完或者无秒杀资格等信息为止。

采用短轮询查询秒杀结果时,在页面上我们同样可以提示用户排队处理中,但是此时客户端会每隔几秒轮询服务器查询秒杀资格的状态,相比于同步下单流程来说,无需长时间占用请求连接。

此时,可能会有网友会问:采用短轮询查询的方式,会不会存在直到超时也查询不到是否具有秒杀资格的状态呢?答案是:有可能! 这里我们试想一下秒杀的真实场景,商家参加秒杀活动本质上不是为了赚钱,而是提升商品的销量和商家的知名度,吸引更多的用户来买自己的商品。所以,我们不必保证用户能够100%的查询到是否具有秒杀资格的状态。

4.秒杀结算

(1)验证下单Token

客户端提交秒杀结算时,会将秒杀Token一同提交到服务器,商城服务会验证当前的秒杀Token是否有效。

(2)加入秒杀购物车

商城服务在验证秒杀Token合法并有效后,会将用户秒杀的商品添加到秒杀购物车。

5.提交订单

(1)订单入库

将用户提交的订单信息保存到数据库中。

(2)删除Token

秒杀商品订单入库成功后,删除秒杀Token。

这里大家可以思考一个问题:我们为什么只在异步下单流程的粉色部分采用异步处理,而没有在其他部分采取异步削峰和填谷的措施呢?

这是因为在异步下单流程的设计中,无论是在产品设计上还是在接口设计上,我们在用户发起秒杀请求阶段对用户的请求进行了限流操作,可以说,系统的限流操作是非常前置的。在用户发起秒杀请求时进行了限流,系统的高峰流量已经被平滑解决了,再往后走,其实系统的并发量和系统流量并不是非常高了。

所以,网上很多的文章和帖子中在介绍秒杀系统时,说是在下单时使用异步削峰来进行一些限流操作,那都是在扯淡! 因为下单操作在整个秒杀系统的流程中属于比较靠后的操作了,限流操作一定要前置处理,在秒杀业务后面的流程中做限流操作是没啥卵用的。

高并发“黑科技”与致胜奇招

假设,在秒杀系统中我们使用Redis实现缓存,假设Redis的读写并发量在5万左右。我们的商城秒杀业务需要支持的并发量在100万左右。如果这100万的并发全部打入Redis中,Redis很可能就会挂掉,那么,我们如何解决这个问题呢?接下来,我们就一起来探讨这个问题。

在高并发的秒杀系统中,如果采用Redis缓存数据,则Redis缓存的并发处理能力是关键,因为很多的前缀操作都需要访问Redis。而异步削峰只是基本的操作,关键还是要保证Redis的并发处理能力。

解决这个问题的关键思想就是:分而治之,将商品库存分开放。

暗度陈仓

我们在Redis中存储秒杀商品的库存数量时,可以将秒杀商品的库存进行“分割”存储来提升Redis的读写并发量。

例如,原来的秒杀商品的id为10001,库存为1000件,在Redis中的存储为(10001, 1000),我们将原有的库存分割为5份,则每份的库存为200件,此时,我们在Redia中存储的信息为(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。

此时,我们将库存进行分割后,每个分割后的库存使用商品id加上一个数字标识来存储,这样,在对存储商品库存的每个Key进行Hash运算时,得出的Hash结果是不同的,这就说明,存储商品库存的Key有很大概率不在Redis的同一个槽位中,这就能够提升Redis处理请求的性能和并发量。

分割库存后,我们还需要在Redis中存储一份商品id和分割库存后的Key的映射关系,此时映射关系的Key为商品的id,也就是10001,Value为分割库存后存储库存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我们可以使用List来存储这些值。

在真正处理库存信息时,我们可以先从Redis中查询出秒杀商品对应的分割库存后的所有Key,同时使用AtomicLong来记录当前的请求数量,使用请求数量对从Redia中查询出的秒杀商品对应的分割库存后的所有Key的长度进行求模运算,得出的结果为0,1,2,3,4。再在前面拼接上商品id就可以得出真正的库存缓存的Key。此时,就可以根据这个Key直接到Redis中获取相应的库存信息。

移花接木

在高并发业务场景中,我们可以直接使用Lua脚本库(OpenResty)从负载均衡层直接访问缓存。

这里,我们思考一个场景:如果在秒杀业务场景中,秒杀的商品被瞬间抢购一空。此时,用户再发起秒杀请求时,如果系统由负载均衡层请求应用层的各个服务,再由应用层的各个服务访问缓存和数据库,其实,本质上已经没有任何意义了,因为商品已经卖完了,再通过系统的应用层进行层层校验已经没有太多意义了!!而应用层的并发访问量是以百为单位的,这又在一定程度上会降低系统的并发度。

为了解决这个问题,此时,我们可以在系统的负载均衡层取出用户发送请求时携带的用户id,商品id和秒杀活动id等信息,直接通过Lua脚本等技术来访问缓存中的库存信息。如果秒杀商品的库存小于或者等于0,则直接返回用户商品已售完的提示信息,而不用再经过应用层的层层校验了。 针对这个架构,我们可以参见本文中的电商系统的架构图(正文开始的第一张图)。

原文链接:https://www.cnblogs.com/binghe001/p/12663557.html

到“秒杀”,恐怕大多数人想到的就是“双 11”,“促销”,“买买买”等火爆的场面吧。

大家为了打折商品蜂拥而至,造成电商网站一片繁华的景象。但作为程序员的我们,看到的却是背后的高并发和可靠性。无论你处在软件开发的哪个阶段,都希望能够设计一套属于自己的秒杀系统。

今天我们一起来看看,一套秒杀系统在架构设计上需要有哪些考量?

秒杀场景的特点

秒杀场景是电商网站定期举办的活动,这个活动有明确的开始和结束时间,而且参与互动的商品是事先定义好了,参与秒杀商品的个数也是有限制的。同时会提供一个秒杀的入口,让用户通过这个入口进行抢购。

总结一下秒杀场景的特点:

  • 定时开始,秒杀时大量用户会在同一时间,抢购同一商品,网站瞬时流量激增。
  • 库存有限,秒杀下单数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 操作可靠,秒杀业务流程比较简单,一般就是下订单减库存。库存就是用户争夺的“资源”,实际被消费的“资源”不能超过计划要售出的“资源”,也就是不能被“超卖”。

系统隔离的设计思路

在分析秒杀的特点后,我们发现秒杀活动是有计划的,并且在短时间内会爆发大量的请求。为了不影响现有的业务系统的正常运行,我们需要把它和现有的系统做隔离。

即使秒杀活动出现问题也不会影响现有的系统。隔离的设计思路可以从三个维度来思考。

业务隔离

既然秒杀是一场活动,那它一定和常规的业务不同,我们可以把它当成一个单独的项目来看。在活动开始之前,最好设计一个“热场”。

“热场”的形式多种多样,例如:分享活动领优惠券,领秒杀名额等等。“热场”的形式不重要,重要的是通过它获取一些准备信息。

例如:有可能参与的用户数,他们的地域分布,他们感兴趣的商品。为后面的技术架构提供数据支持。

技术隔离

技术隔离架构图

前面有了准备工作,那么从技术上需要有以下几个方面的考虑:

  • 客户端,前端秒杀页面使用专门的页面,这些页面包括静态的 HTML 和动态的 JS,他们都需要在 CDN 上缓存。
  • 接入层,加入过滤器专门处理秒杀请求,即使我们扩展再多的应用,使用再多的应用服务器,部署再多的负载均衡器,都会遇到支撑不住海量请求的时候。

所以,在这一层我们要考虑的是如何做好限流,当超过系统承受范围的时候,需要果断阻止请求的涌入。

  • 应用层,瞬时的海量请求好比请求的“高峰”,我们架构系统的目的就是“削峰”。

需要使用服务集群和水平扩展,让“高峰”请求分流到不同的服务器进行处理。同时,还会利用缓存和队列技术减轻应用处理的压力,通过异步请求的方式做到最终一致性。

由于是多线程操作,而且商品的额度有限,为了解决超卖的问题,需要考虑进程锁的问题。

数据库隔离

秒杀活动持续时间短,瞬时数据量大。为了不影响现有数据库的正常业务,可以建立新的库或者表来处理。

在秒杀结束以后,需要把这部分数据同步到主业务系统中,或者查询表中。如果数据量特别巨大,到千万级别甚至上亿,建议使用分表或者分库。

客户端设计

上面提到的三个隔离维度中,我们对技术维度是最为关心的。如果说浏览器/客户端是用户接触“秒杀系统”的入口,那么在这一层提供缓存数据就是非常必要的。

在设计之初,我们会为秒杀的商品生成专门的商品页面和订单页面。这些页面以静态的 HTML 为主,包括的动态信息尽量少。

从业务的角度来说,这些商品的信息早就被用户熟识了,在秒杀的时候,他们关心的是如何快速下单。

既然商品的详情页面和订单页面都是静态生成的,那么就需要定义一个 URL,当要开始秒杀之前,开放这个 URL 给用户访问。

为了防止“程序员或者内部人员”作弊,这里的地址可以通过时间戳和 Hash 算法来生成,也就是说这个地址只有系统知道,到了快秒杀之前才由系统发放出去。

有人说浏览器/客户端如果存放的都是静态页面,那么“控制开始下单”的按钮,以及发送“下单请求”的按钮,也是静态的吗?

答案是否定的,其实静态页面是方便客户端好缓存,下单的动作以及下单时间的控制还是在服务器端。

只不过是通过 JS 文件的方式发送给客户端,在快要秒杀之前,会把这部分 JS 下载到客户端。

因为,其业务逻辑很少,基本只包括时间,用户信息,商品信息等等。所以,其对网络的要求不高。

同时,在网络设计上,我们也会将 JS 和 HTML 同时缓存在 CDN 上面,让用户从离自己最近的 CDN 服务器上获取这些信息。

为了避免秒杀程序参与秒杀,在客户端也会设计一些问答或者滑块的功能,减少此类机器人对服务器的压力。

秒杀系统前端设计简图

代理层设计

说完了秒杀系统的前端设计,请求自然地来到了代理层。由于用户的请求量大,我们需要用负载均衡加上服务器集群,来面对如此空前的压力。

代理层三大功能简图

在这一层是可以做缓存,过滤和限流的:

  • 缓存,以 Nginx 为例,它可以缓存用户的信息。假设用户信息的修改没有那么频繁,即使有类似的修改也可以通过更新服务来刷新。总比从服务器上获取效率要高得多。
  • 过滤,既然缓存了用户信息,这里就可以过滤掉一些不满足条件的用户。注意,这里的用户信息的过滤和缓存只是一个例子。主要想表达的意思是,可以将一些变化不频繁的数据,提到代理层来缓存,提高响应的效率。同时,还可以根据风控系统返回的信息,过滤一些疑似机器人或者恶意请求。例如:从固定 IP 过来的,频率过高的请求。最重要的就是在这一层,可以识别来自秒杀系统的请求。如果是带有秒杀系统的参数,就要把请求路由到秒杀系统的服务器集群。这样才能和正常的业务系统分割开来。
  • 限流,每个服务器集群能够承受的压力都是有限的。代理层可以根据服务器集群能够承受的最大压力,设置流量的阀值。阀值的设置可以是动态调整的。例如:集群服务器中有 10 个服务器,其中一台由于压力过大挂掉了。此时就需要调整代理层的流量阀值,将能够处理的请求流量减少,保护后端的应用服务器。当服务器恢复以后,又可以将阀值调回原位。可以通过 Nginx+Lua 合作完成,Lua 从服务注册中心读取服务健康状态,动态调整流量。

应用层设计

“秒杀系统”秒杀的是什么?无非是商品。对于系统来说就是商品的库存,购买的商品一旦超过了库存就不能再卖了。

防止超卖

超过了库存还可以卖给用户,这就是“超卖”,也是系统设计需要避免的。为了承受大流量的访问,我们用了水平扩展的服务,但是对于他们消费的资源“库存”来说,却只有一个。

为了提高效率,会将这个库存信息放到缓存中。以流行的 Redis 为例,用它存放库存信息,由多个线程来访问就会出现资源争夺的情况。也就是分布式程序争夺唯一资源,为了解决这个问题我们需要实现分布式锁。

假设这里有多个应用响应用户的订单请求,他们同时会去访问 Redis 中存放的库存信息,每接受用户一次请求,都会从 Redis 的库存中减去 1 个商品库存量。

当任何一个进程访问 Redis 中的库存资源时,其他进程是不能访问的,所以这里需要考虑锁的情况(乐观,悲观)。

Redis 缓存承载库存变量

如果锁长期没有释放,需要考虑锁的过期时间,需要设置两个超时时间:

  • 资源本身的超时时间,一旦资源被使用一段时间还没有被释放,Redis 会自动释放掉该资源给其他服务使用。
  • 服务获取资源的超时时间,一旦一个服务获取资源一段时间后,不管该服务是否处理完这个资源,都需要释放该资源给其他服务使用。

订单处理流程

这里的“扣减服务”完成了最简单的扣减库存工作,并没有和其他项目服务打交道,更没有访问数据库。

订单流程示意图

后面的流程相对比较复杂,我们先看图,根据图示来讲解:

  • 首先,扣减服务作为下单流程的入口,会先对商品的库存做扣减。同样它会检查商品是否还有库存?
  • 由于订单对应的操作步骤比较多,为了让流量变得平滑,这里使用队列存放每个订单请求,等待订单处理服务完成具体业务。
  • 订单处理服务实现多线程,或者水平扩展的服务阵列,它们不断监听队列中的消息。一旦发现有新订单请求,就取出订单进行后续处理。注意,这里可以加入类似 ZooKeeper 这样的服务调度来帮助,协调服务调度和任务分配。
  • 订单处理服务,处理完订单以后会把结果写到数据库。写数据库是 IO 操作,耗时长。
  • 所以,在写数据库的同时,会把结果先写入缓存中,这样用户是可以第一时间查询自己是否下单成功了。
  • 结果写入数据库,这个操作有可能成功也有可能失败。
  • 为了保证数据的最终一致性,我们用订单结果同步的服务不断的对比,缓存和数据库中的订单结果信息。一旦发现不一致,会去做重试操作。如果重试依旧不成功,会重写信息到缓存,让用户知道失败原因。
  • 用户下单以后,焦虑地刷新页面查看下单的结果,实际上是读到的缓存上的下单结果信息。

虽然,这个信息和最终结果有偏差,但是在秒杀的场景,要求高性能是前提,结果的一致性,可以后期补偿。

数据库设计

讲完了秒杀的处理流程,来谈谈数据库设计要注意的点。

数据估算

前面说了秒杀场景需要注意隔离,这里的隔离包括“业务隔离”。就是说我们在秒杀之前,需要通过业务的手段,例如:热场活动,问卷调查,历史数据分析。通过他们去估算这次秒杀可能需要存储的数据量。

这里有两部分的数据需要考虑:

  • 业务数据
  • 日志数据

前者不言而喻是给业务系统用的。后者,是用来分析和后续处理问题订单用的,秒杀完毕以后还可以用来复盘。

分表分库

对于这些数据的存放,需要分情况讨论,例如,MySQL 单表推荐的存储量是 500W 条记录(经验数字)。

如果估算的时候超过了这个数据,建议做分表。如果服务的连接数较多,建议进行分库的操作。

数据隔离

由于大量的数据操作是插入,有少部分的修改操作。如果使用关系型数据来存储,建议用专门的表来存放,不建议使用业务系统正在使用的表。

这个开头提到了,数据隔离是必须的,一旦秒杀系统挂了,不会影响到正常业务系统,这个风险意识要有。表的设计除了 ID 以外,最好不要设置其他主键,保证能够快速地插入。

数据合并

由于是用的专用表存储,在秒杀活动完毕以后,需要将其和现有的数据做合并。其实,交易已经完成,合并的目的也就是查询。

这个合并需要根据具体情况来分析,如果对于那些“只读”的数据,对于做了读写分离的公司,可以导入到专门负责读的数据库或者 NoSQL 数据库中。

压力测试

构建了秒杀系统,一定会面临上线,那么在上线之前压力测试是必不可少的。

我们做压力测试的目的是检验系统崩溃的边缘在哪里?系统的极限在哪里?

这样才能合理地设置流量的上限,为了保证系统的稳定性,多余的流量需要被抛弃。

压力测试的方法

合理的测试方法可以帮助我们对系统有深入的了解,这里介绍两种压力测试的方法:

  • 正压力测试
  • 负压力测试

正压力测试。每次秒杀活动都会计划,使用多少服务器资源,承受多少的请求量。

可以在这个请求量上面不断加压,直到系统接近崩溃或者真正崩溃。简单的说就是做加法。

负压力测试。在系统正常运行的情况下,逐步减少支撑系统的资源(服务器),看什么时候系统无法支撑正常的业务请求。

例如:在系统正常运行的情况下,逐步减少服务器或者微服务的数量,观察业务请求的情况。说白了就是做减法。

压力测试的步骤

测试步骤

有了测试方法的加持,我们来看看需要遵循哪些测试步骤。下面的操作偏套路化,大家在其他系统的压力测试也可以这么做,给大家做个参考。

第一,确定测试目标。与性能测试不同的是,压力测试的目标是,什么时候系统会接近崩溃。比如:需要支撑 500W 访问量。

第二,确定关键功能。压力测试其实是有重点的,根据 2/8 原则,系统中 20% 的功能被使用的是最多的,我们可以针对这些核心功能进行压力测试。例如:下单,库存扣减。

关注核心服务

第三,确定负载。这个和关键服务的思路一致,不是每个服务都有高负载的,我们的测试其实是要关注那些负载量大的服务,或者是一段时间内系统中某些服务的负载有波动。这些都是测试目标。

第四,选择环境,建议搭建和生产环境一模一样的环境进行测试。

第五,确定监视点,实际上就是对关注的参数进行监视,例如 CPU 负载,内存使用率,系统吞吐量等等。

第六,产生负载,这里需要从生产环境去获取一些真实的数据作为负载数据源,这部分数据源根据目标系统的承受要求由脚本驱动,对系统进行冲击。

建议使用往期秒杀系统的数据,或者实际生产系统的数据进行测试。

第七,执行测试,这里主要是根据目标系统,关键组件,用负载进行测试,返回监视点的数据。

建议团队可以对测试定一个计划,模拟不同的网络环境,硬件条件进行有规律的测试。

第八,分析数据,针对测试的目的,对关键服务的压力测试数据进行分析得知该服务的承受上限在哪里。

对一段时间内有负载波动或者大负载的服务进行数据分析,得出服务改造的方向。

总结

秒杀系统的特点,并发量大,资源有限,操作相对简单,访问的都是热点数据。因此,我们需要把它从业务,技术,数据上做隔离,保证不影响到现有的系统。

因此,架构设计需要分几层来考虑,从客户请求到数据库存储,到最后上线前的压力测试。

简易的思维导图送给大家

思考顺序如下,客户端→代理层→应用层→数据库→压力测试:

客户端 90% 静态 HTML+10% 动态 JS;配合 CDN 做好缓存工作。

接入层专注于过滤和限流。

应用层利用缓存+队列+分布式处理好订单。

做好数据的预估,隔离,合并。

上线之前记得进行压力测试。

作者简介

崔皓,十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。