秒杀架构

在讲解新场景之前,先来回顾一下前面几个场景的内容。

读缓存场景中,项目组先把数据存放在缓存中,每次请求通过缓存读取数据,大大减小了数据库的读请求压力;写缓存场景中,碰到流量洪峰时,先将数据写入缓存中,再逐步迁移数据到数据库,大大减小了数据库的写请求压力;数据收集场景中,利用消息队列可以把缓存中的数据迁移到数据库中。这3个场景中涉及的架构设计思路,本章场景都会用到。

业务场景:设计秒杀架构必知必会的那些事

先来看一个实际的业务场景。

某一次公司策划了一场秒杀活动,该活动提供了100件特价商品(商品价格非常低),供用户于当年10月10日22点10分0秒开始秒杀。

当时平台已经积累了几千万的用户量,预计数十万的用户对这些特价商品感兴趣。根据经验,特价商品一般会在1~2秒内被一抢而光,剩余时间涌进来的流量用户只能看到秒杀结束界面,因此预测秒杀开启那一瞬间会出现一个流量峰值。

这也是一场临时性的活动,领导要求别加太多服务器,也别花太多时间重构架构,也就是说需要以最小的技术代价来应对这次秒杀活动。

因此,这次秒杀架构的设计目标是以较小的改动来保证秒杀时的流量洪流不会冲垮服务器。

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

那如何做到遵循这4个原则呢?先从整体思路入手吧。

整体思路

其实秒杀架构的设计方案就是一个不断过滤请求的过程。从系统架构层面来说,秒杀系统的分层设计思路如图7-1所示。在图7-1中发现,秒杀系统的架构设计目标是尽量在上层处理用户请求,不让其往下层游动。那具体如何实现呢?

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

这里通过一张图来描述秒杀系统的具体业务流程,如图7-2所示。

设计秒杀系统的思路(秒杀架构业务场景)(1)

• 图7-1 秒杀系统分层思路

设计秒杀系统的思路(秒杀架构业务场景)(2)

• 图7-2 秒杀系统业务流程

接下来就按照秒杀系统的业务流程,来一步步讲解如何将请求拦截在系统上游。

浏览页面如何将请求拦截在上游

在以往的秒杀系统架构经历中,曾出现过这么一种状况:当时把系统的方方面面都考虑到了,但是活动一上线,第三方监控系统就显示异常,检查后发现所有服务器的性能指标都没问题,唯独出口带宽有问题,它被占满了。

结果就是用户参与活动时页面出现严重卡顿,用户抱怨不断。

一朝被蛇咬,十年怕井绳。有了这次惨痛经历,带宽这件事情就被牢牢记在脑子里,所以在之后的项目中,静态资源尽量使用CDN(内容分发网络),如果涉及PC网站,还必须首先进行前后端分离。

说到这里,先简单介绍一下什么是CDN。

比如,请求访问的地址是https://static.weimu.xx/1.jpg,这个地址指向自己的服务器,经过改造后,static.weimu.xx这个域名的解析就会交给CDN服务商。

CDN服务商在全国各地都有服务器,服务器中存放着静态资源的缓存。CDN收到这个域名后,首先会寻找一台响应最快的服务器,并指向这个服务器的IP。如果对细节感兴趣,可以查阅其他资料。

因此,使用CDN的好处是不用花费自己的服务器资源和带宽,且响应速度快。通过这种方式,可以把静态资源的压力拦截在系统分层的外面。

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

1 ) 评 论 、 商 品 详 情 、 购 买 数 量 等 相 关 的 请 求 , 一 般 都 是 通 过JS(JavaScript)在后台动态调用。在这个场景中,可以把动态的数据与页面进行整合,比如把每个秒杀商品的详情页面变成静态页面,然后再放入CDN。

如果觉得改造太大,也可以把它放在Redis缓存中,不过笔者更倾向于CDN。

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

3)判断秒杀结束。具体做法是将秒杀结束的标识放在Cookie中,如果Cookie中没有结束标识,请求就会进入后台服务器,后台服务器判断本地内存没有结束标识,就会进入缓存,如果缓存中也没有结束标识,那就说明秒杀没有结束。总体来说,对于浏览页面的用户行为,需要把用户请求尽量拦截在CDN、静态资源或负载均衡侧,如果确实做不到,也要拦截在缓存中。

下单页面如何将请求拦截在上游

用户进入下单页面时,主要有两个操作动作:进入下单页面、提交订单。

下面讲解如何在这两个环节中将请求拦截在系统上游。

1.进入下单页面

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

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

2)用户点击下单页面的购买按钮后,将此按钮设为Disable(不可用),防止用户不断点击它。

2.提交订单

秒杀系统架构方案的核心是订单提交,因为这个步骤的逻辑最复杂,而其他步骤仅涉及页面展示的逻辑,针对高并发问题使用缓存或者CDN进行处理难度不大。

因此,在订单提交环节,要想尽一切办法在系统各个分层中把一些不必要的请求过滤掉。

(1)网关层面过滤请求

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

那在网关层面如何实现请求过滤呢?可以做3种限制。

1)限定每个用户的访问频率,比如每5秒下单一次。

2)限定每个IP的访问频率。这种方式是为了避免有人通过机器人自动下单,导致错杀真实用户。

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

前两种限制比较简单,在nginx上就能快速完成配置,第3种限流方式也不复杂,相关的原理在后面的章节会讲。

(2)后台服务器过滤请求

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

具体如何实现呢?主要考虑以下4点。

1)商品库存放入缓存Redis中:如果每个请求都前往数据库查询商品库存,数据库将无法承受,因此需要把商品库存放在缓存中,这样每次用户下单前,就先使用decr操作扣减库存,判断返回值。如果Redis的库存扣减后小于0,说明秒杀失败,将库存用incr操作恢复;如果Redis的库存扣减后不小于0,说明秒杀成功,开始创建订单。

把库存放入Redis时,下单的逻辑都是基于缓存的库存为第一现场。但是如果这时候有别的服务或者代码修改了数据库里面的库存,怎么办?这时的做法就是确保在秒杀期间不做上架或修改库存之类的业务操作,即不通过技术,而是通过业务流程来保证。

2)订单写入缓存中:在第5章介绍写缓存时提过一个方案,即订单数据先不放入数据库,而是放到缓存中,然后每隔一段时间(比如100毫秒)批量插入一批订单。用户下单后,首先进入一个等待页面,然后这个页面向后台定时轮询订单数据。轮询过程中,后台先在Redis中查询订单数据,查不到就说明数据已经落库,再去数据库查询订单数据,查到后直接返回给用户,用户收到消息通知后可以直接进入付款页面支付;在数据库查询订单数据时,查不到说明秒杀失败(理论上不会查不到,如果一直查不到就需要抛出异常并跟踪处理)。

3)订单批量落库:需要定期将订单批量落库,且在订单落库时扣减数据库中的库存。这个做法和第6章中的写缓存一样,这里不再重复。

4)Redis停止工作(挂掉)怎么办:虽然讲了这么多关于后台服务器的逻辑,在秒杀架构里面,最重要的反而是网关层的限流,它挡住了大部分的流量,进入后台服务器的流量并不多。不过仍然要考虑针对Redis停止工作的情况,分别处理前面的3种状况。

比如读Redis中的库存时,如果失败了,那就让它直接去数据库扣减库存,把那些incr和decr的逻辑放到数据库去;若是把订单写入缓存的时候失败了,那就直接将订单数据写入数据库中,然后就不需要处理后面批量落库的逻辑了。

以上就是订单提交操作的架构设计,不难看出它主要是在网关层和后台服务器进行相关设计。

付款页面如何将请求拦截在上游

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

关于数据的一致性,后面的章节会专门展开,这里就不单独讨论了。

整体服务器架构

再来回顾一下秒杀系统的分层思路,这也是秒杀系统的整体服务器架构方案,如图7-3所示。

设计秒杀系统的思路(秒杀架构业务场景)(3)

• 图7-3 秒杀系统分层思路

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

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

小结

到这里,秒杀架构的注意事项就讲完了。因为很多注意事项在前面几个场景一一介绍过,所以这一章讲解的内容就比较简练。

表7-1中整理了一份秒杀系统设计Checklist,供大家参考。

设计秒杀系统的思路(秒杀架构业务场景)(4)

表7-1 秒杀系统设计Checklist

这个场景中还有以下3个要点需要注意。

1)假设后台某服务因秒杀崩溃了,如何避免其他服务雪崩?这一点会在第10章详细展开。

2)网关层的限流。这一点会在第11章详细展开。

3)付款的数据一致性。这一点会在第13章详细展开。

这个秒杀项目上线以后,通过观察后台日志、内部监控平台的数据和第三方监控的数据,并用脚本对比库存扣减情况和订单情况,发现一切正常。因此,这次的秒杀架构是成功的。

其实笔者之前也做过秒杀架构,但是那时候的逻辑是,必须保证前面100名的客户可以抢到商品,并没有做限流等措施,因此后台服务器的压力很大,经常出问题。

虽然要保证前100名客户的订单成功,但是前面100名不一定就是第一时间点击秒杀按钮的客户,有些人网速快,有些人网速慢。另外,普通客户肯定没有专门“薅羊毛”的人操作快。对于普通客户来说,随机决定比单纯比快要更有机会下单成功。

后来,基本上秒杀架构都会设计限流。有一些秒杀的代码是在前端的JS中随机抛弃掉一些请求,这其实也是某种意义上的限流,只不过不太合理,应尽量避免。

秒杀架构场景就说到这里。接下来是第3部分基于常见组件的微服务场景实战,将从最简单的服务管理说起,由浅入深,逐层讲解微服务的相关知识。

本文给大家讲解的内容是缓存层场景实战,秒杀架构,业务场景:设计秒杀架构必知必会的那些事以及整体的思路
  1. 下篇文章给大家讲解的内容是基于常见组件的微服务场景实战,注册发现,业务场景:如何对几十个后台服务进行高效管理
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
,