1. Overview
1.1 并发读写
秒杀要解决的主要问题是:并发读与并发写。
并发读的优化理念是尽量减少用户到服务端来读数据,或者让他们读更少的数据;并发写的处理原则一样,要求我们在数据库层面独立出一个库,做特殊的处理。
其次,还需要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
1.2 API设计原则
值得注意的地方是:如果想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上从浏览器到服务端我们要遵循几个原则,就是保证**用户请求的数据尽量少、请求数尽量少、路径尽量短、依赖尽量少,不要有单点**
1.3 秒杀架构原则
1.3.1 高可用
整个系统架构需要满足高可用性,流量符合预期的时候肯定要稳定,就是超出预期也同样不能掉链子,保证秒杀产品顺利卖出。
1.3.2 一致性
数据必须一致,即成交总量必须和设定的数量一致。
1.3.3 高可用
系统的性能要足够强,支撑足够大的流量,不仅是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方都要快一点,整个系统就完美了。
本文将从这三个原则上来分别进行详细说明。
2. 架构原则
秒杀系统本质上是一个满足大并发、高性能和高可用的分布式系统。
2.1 数据尽量少
用户请求的数据能少就少,请求的数据包括上传给系统的数据和系统返回给用户的数据。
因为这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器处理,而服务器在写网络的时候通常都要做压缩和字符编码,这些都非常消耗CPU,所以减少传输的数据量可以显著减少CPU的使用。
同样,数据尽量少还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。调用其他服务会涉及数据的序列化和反序列化,这也是CPU的一大杀手,同样也会增加延时。而且数据库本身也很容易成为瓶颈,因此越少和数据库打交道越好。
2.2 请求数尽量少
用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。所以你要记住的是,减少请求数可以显著减少以上这些因素导致的资源消耗。
例如,减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 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](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,然后动态把这些文件合并起来一起返回。
2.3 路径要尽量短
路径指的是用户发出请求到返回数据这个过程中需要经过的中间节点的数量。
通常,这些节点可以表示为一个系统或者一个新的 Socket 连接(比如代理服务器只是创建一个新的 Socket 连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。
然而,每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的话,那么整个请求的可用性是:99.9% 的 5 次方,约等于 99.5%。
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。
要缩短访问路径可以将多个相互有强依赖的应用合并部署在一起,将远程过程调用变成JVM内部的方法调用。
2.4 依赖要尽量少
所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务。
举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。
要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。
注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。
2.5 不要有单点
不能有单点,因为单点意味着没有备份,风险不可控,设计分布式系统的一个最重要的原则就是消除单点。
如何避免单点?—-> 避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动了。
如何那把服务的状态和机器解耦呢?这里也有很多实现方式。例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。
应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景一般要通过冗余多个备份的方式来解决单点问题。
3. 不同场景下的不同架构案例
如果你想快速搭建一个简单的秒杀系统,只需要把你的商品购买页面增加一个“定时上架”功能,仅在秒杀开始时才让用户看到购买按钮,当商品的库存卖完了也就结束了。这就是当时第一个版本的秒杀系统实现方式。
但随着请求量的加大(比如从 1w/s 到了 10w/s 的量级),这个简单的架构很快就遇到了瓶颈,因此需要做架构改造来提升系统性能。这些架构改造包括:
-
把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化,例如这个独立出来的系统就减少了店铺装修的功能,减少了页面的复杂度;
-
在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;
-
将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
-
增加秒杀答题,防止有秒杀器抢单。
此时秒杀已经成为了一个独立的新系统,另外核心的一些数据放到了缓存当中,其他的关联系统也都以独立集群的方式进行部署。
但是这个架构仍然无法支持超过100w/s的请求量,因此为了进一步提高秒杀系统的性能,又对架构做了进一步的升级,比如:
-
对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
-
在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
-
增加系统限流保护,防止最坏的情况发生
此时整个系统架构变成了这个样子,已经对页面进行了进一步的静态化,秒杀过程当中就不需要刷新整个页面了,只需要向服务端请求很少的动态数据。而且最关键的详情和交易系统都增加了本地缓存,来提前缓存秒杀商品的信息,热点数据库也做了独立部署。
从前面的几次升级来看,其实越到后面需要定制的地方越多,也就是越“不通用”。例如,把秒杀商品缓存在每台机器的内存中,这种方式显然不适合太多的商品同时进行秒杀的情况,因为单机的内存始终有限。所以要取得极致的性能,就要在其他地方(比如,通用性、易用性、成本等方面)有所牺牲。
4. 动静分离的方案
秒杀系统需要让请求效率足够高 - 提高单次请求的效率,减少没必要的请求。
4.1 何为动静数据
将用户请求的数据(如HTML)划分为动态数据和静态数据。而动态静态数据的划分,在于看页面中输出的数据是否和URL,浏览者,时间,地域相关,以及是否含有Cookie等私密数据。
-
对很多媒体类的网站来说,无论谁来看文章,展示的数据都是一样的,那么哪怕这是个动态页面,它仍然是个典型的静态数据。
-
访问淘宝的首页,每个人看到的页面可能都是不一样的,其中包含了很多根据访问者个人信息进行的推荐,这些个性化的数据就称为动态数据。
这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面&#