软件架构场景之—— 秒杀架构:设计秒杀架构必知必会的那些事

业务场景

之前,公司策划了一场秒杀活动,该活动提供了 100 件特价商品(商品价格非常低)供用户于 10 月 10 日 22 点 10 分 0 秒正式参与秒杀

当时,平台已经积累了几千万的用户量,预计数十万的用户对此特价商品感兴趣。按照秒杀活动的调性,特价商品一般会在 1-2 秒内被一抢而光,剩余时间涌进来的流量只能看到秒杀结束界面,因此我们预测秒杀开启那一瞬间会出现一个瞬间流量峰值。这也是一场短暂性的活动,需要以最小的技术代价搞定这次秒杀活动。因此,这次秒杀架构的设计目标是以较小的改动保证秒杀时间的流量洪流不会冲垮服务器

对于秒杀架构设计而言,其难点在于僧多粥少,因此设计秒杀架构时,我们一般需要遵循东西不能超卖、下单成功的订单数据不能丢失、服务器和数据库不能挂、尽量别让机器人抢走商品这 4 个原则

整体思路

秒杀架构的设计方案就是一个不断过滤请求的过程。 从系统架构层面来说,秒杀系统的分层设计思路如下图所示

在上图中我们发现,秒杀系统的架构设计目标是尽量在上层把用户的请求处理掉,不让其往下层游动,那具体如何实现呢?

由于整个秒杀系统涉及多个用户操作步骤,因此解决如何将请求拦截在系统上层这个问题时,我们需要结合实际业务流程,将用户的每个操作步骤考虑在内

(一)浏览页面如何将请求拦截在上游?

在以往的秒杀系统架构经历中,曾出现过这么一个事故:当时把系统的方方面面都考虑到了,检查完自我感觉很好,可是活动一上线,系统立马显示异常,一通检查后,我们发现所有服务器的性能指标都没问题,唯独出口带宽出问题了。(它被占满了)结果你们也知道,参与活动时页面出现严重卡顿,用户吐槽不断

通过这次惨痛经历,将设计思路进行了相关调整,后期对于静态资源能上 CDN 就上 CDN。如果涉及 PC 网站,首先必须前后端分离,然后静态资源能上 CDN 就上 CDN

什么是 CDN?

比如平时访问的请求是 https://static.vincent/1.jpg,这个地址指向自己的服务器,经过改造后,我们将 static.vincent 这个域名解析交给 CDN 服务商。因 CDN 服务商在全国各地都有服务器,服务器中存放着我们想要的静态资源的缓存。CDN 收到这个域名后,首先会寻找一台响应最快的服务器,并指向这个服务器的 IP,因此,使用 CDN 的好处是不浪费自己的服务器资源和带宽,且响应速度快。通过这种方式,我们可以把静态资源的压力拦截在系统分层的外面

那如果是动态的请求该怎么办?有如下 3 种实现方式

  1. 比如评论、商品属性详情、购买数等请求,平时我们都是通过 JS 后台动态调用。在这个场景中,我们可以把动态的数据与页面进行整合,比如把每个秒杀商品的详情页面变成静态页面,然后再放入 CDN。如果嫌改造太大,我们也可以把它放在 Redis 缓存中,不过我更倾向于 CDN

  2. 判断服务器时间开启秒杀的标识:一般页面中都有一个 JS,它通过访问服务器获取服务器时间,然后根据时间开启秒杀下单的按钮,即判断秒杀开始时,我们会将下单按钮设置为可以购买。针对获取服务器时间的这个请求,我们把它放在静态资源或负载均衡那层即可,这样用户请求就不会进入系统下游

  3. 判断秒杀结束:我们的做法是将秒杀结束的标识放在 cookie 中,如果 cookie 中没有结束标识,请求就会进入后台服务器,后台服务器判断本地内存没有结束标识,就会进入缓存中,要是缓存中也没有结束标识,那就说明秒杀没结束

总的来说,对于浏览页面的用户行为,我们需要把用户请求尽量拦截在 CDN、静态资源或负载均衡侧,实在不行可以拦截在缓存中

(二)下单页面如何将请求拦截在上游?

用户进入下单页时,主要分为 2 个操作动作:进入下单页、订单提交

  • 进入下单页

为了防止别人通过爬虫抓取下单页页面信息,从而给服务器增加压力,因此我们需要在下单页中做以下 2 层防护,以此防止(恶意)请求重复提交

  1. 页面 URL 后台动态获取:按照正常的活动设计流程,用户只有在秒杀活动开启后才可进入下单页,但难免有同行在活动开启前直接获取下单页的 URL 并不断刷新,这样恶意请求就跑到了后台服务器中。虽然后台服务器也可以拦截恶意请求,但是徒增了不少压力。此时我们主要使用一个诡异的 URL 进行处理。(我们不把它放在静态页面中,而是通过后台动态获取。)前面我们介绍了 JS 可以用来判断秒杀开始时间 ,秒杀时间一到,它便可以通过另一个请求获取这个 URL

  2. 用户点击下单页的购买按钮直接 disable,因此我们还需要防止用户不断点击购买按钮

  • 订单提交

秒杀系统架构方案的重心是订单提交,因为订单提交这个步骤的逻辑最复杂,其他步骤仅是页面展示的逻辑,针对高并发问题使用缓存或者 CDN 进行处理难度不大,因此,在订单提交环节,要想尽一切办法在系统各个分层中把一些不必要的请求过滤掉

  • 网关层面过滤请求

对系统而言,如果我们可以在网关层面拦截掉用户请求,可以说这个方案的性价比很高。要是能在这层过滤 95% 以上的请求,整个系统也就很稳定

那在网关层面如何实现请求过滤呢?

  1. 限定每个用户访问频率:比如每 5 秒下单 1 次

  2. 限定每个 IP 访问频率:这种方式我们担心有些人通过机器人自动下单,因此错杀真实用户

  3. 把一个时间段内的请求拦截掉一个百分比,或者只允许特定数量的请求进入后台服务器(这里可以使用限流的漏桶或令牌桶算法)

  • 后台服务器过滤请求

请求进入后台服务器后,我们的目标已经不是如何过滤请求了,而是如何保证特价商品不超卖,以及如何保证特价商品订单数据的准确性

那具体如何实现呢?

  1. 商品库存放入缓存 Redis 中: 如果每个请求都前往数据库查询商品库存,数据库肯定扛不住,因此我们需要把库存存放在缓存中,这样每次用户下单前,先使用 decr 扣减库存,判断返回值。如果 Redis 的库存扣减后 <0,说明秒杀失败,库存 incr 回去;如果 Redis 的库存扣减后 >=0,说明秒杀成功,开始创建订单

  2. 订单写入缓存中: 订单数据先不放入数据库,而是先放到缓存中,然后每隔一段时间(比如 100ms )批量插入订单。用户下单后,首先进入一个等待页面,然后这个页面向后台定时轮询订单数据。轮询订单数据的过程中,后台先在 Redis 中查询订单数据,查不到说明已经落库,再去数据库查询订单数据,查到后直接返回给用户,用户收到消息通知后可以直接进入付款页面支付了。在数据库查询订单数据时,查不到说明秒杀失败(理论上不会查不到,你要是一直查不到记得抛个异常后续跟踪处理下)

  3. 订单批量落库: 我们需要定期将订单批量落库,且在订单落库的时扣减数据库中的库存

(三)付款页如何将请求拦截在上游?

在付款页面,基本不需要再过滤用户请求了。在这个环节,除了保障数据的一致性,还需要注意一个要点:如果业务逻辑上出现一个订单未及时付款而被取消,记得把数据库及 Redis 的库存加回去

整体服务器架构

为了保障秒杀系统的高可用性,在整体服务器架构中,需要保证上图中所有的层级都是高可用。因此,静态资源服务器、网关、后台服务器均需要配置负载均衡,而缓存 Redis 和数据库均需要配置集群模式

整体服务器架构中还有一个重要组成部分——MQ,因这次的秒杀架构方案中不涉及它的设计逻辑,所以我们并未在上面的分层中提及它。不过,服务间触发通知时,我们就需要使用它了,因此我们也需要保证它是高可用的(这里我们要把主从、分片、failover机制都考虑进去)

总结

下表中整理了一份秒杀架构 check list,在你设计秒杀系统时,这些可能对你有所帮助

流程 事项
浏览页面 静态资源放CDN
浏览页面 秒杀期间一些动态数据请求放进静态页面
浏览页面 秒杀开始的时间获取依赖服务端
浏览页面 秒杀结束的标识放在各个地方
下单 下单URL动态后台获取
下单 购买按钮点击完置灰
下单 网关从3个方面过滤请求:1、用户访问频率  2、IP访问频率  3、整体流量控制
下单 库存放Redis,每次判断缓存防止超卖
下单 订单先放缓存在批量落库
付款 订单取消记得加回数据库和 Redis 的库存
服务器架构 静态资源服务器负载均衡
服务器架构 网关负载均衡
服务器架构 后台服务负载均衡
服务器架构 Redis 集群
服务器架构 MQ 集群
服务器架构 数据库集群

猜你喜欢

转载自blog.csdn.net/vincent_wen0766/article/details/112524169
今日推荐