《如何设计一个秒杀系统》——专栏笔记

秒杀系统架构设计的关键点

“秒杀系统”通常是与所谓的“商品系统”相互独立的、隔离的。秒杀其实主要解决两个问题,一个是并发读,一个是并发写,具体实现方式如下:

  • 并发读:尽量减少用户到服务端来“读”数据,或者让他们读更少的数据。
  • 并发写:可以在数据库层面独立出来一个库,做特殊的处理。

从一个架构师的角度来看,要想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上从浏览器到服务端我们要遵循几个原则

  1. 要保证用户请求的数据尽量少
  2. 请求数尽量少
  3. 路径尽量短
  4. 依赖尽量少
  5. 不要有单点(单个节点,即没有备份)

上述5点是本篇文章的关键点。

秒杀的整体架构可以概括为“稳、准、快”几个关键字:

  • 稳:高可用
  • 准:保证数据的一致性,不能出现如“超卖”的现象
  • 快:性能高

设计秒杀系统时应该注意的5个架构原则

架构原则——“4 要 1 不要”

1、数据要尽量少

用户请求的数据尽量少,该“请求的数据”,包含:

  1. 上传给系统的数据
  2. 系统返回给用户的数据
  3. 系统依赖的数据

为什么要少呢?

  1. 这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。(简化秒杀页面、去除不必要的页面装饰)
  2. 系统依赖的数据可能来自别的服务器或是数据库,调用其他服务会涉及数据的序列化和反序列化,而这也是 CPU 的一大杀手,同样也会增加延时。

2、请求数要尽量少

用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外的请求,比如说,这个页面依赖的 CSS/JavaScript、图片等。浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。

解决方案减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开(https://g.xxx.com/tm/xxb/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,modulefocus/index.xtpl.js)。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL,然后动态把这些文件合并起来一起返回。

3、路径要尽量短

所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。通常,这些节点可以表示为一个系统或者一个新的 Socket 连接,每增加一个连接都会增加新的不确定性。

假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的话,那么整个请求的可用性是:99.9% 的 5 次方,约等于 99.5%。

所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。

解决方案:要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用 (RPC)变成 JVM 内部之间的方法调用。

4、依赖要尽量少

所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖

案例:比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息。而至于其他的信息,如优惠券、成交列表等,这些对秒杀不是必需的信息(弱依赖)在紧急情况下就可以去掉。

实现方式:要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。
注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。(例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。)

5、不要有单点

不要有单点,即不要用单机的架构,单点意味着没有备份,风险不可控。我们要做的,就是“消除单点”。

消除单点的关键在于避免服务的状态和机器绑定,所以我们要让服务无状态化,这样服务就可以在机器中随意移动访问。

实现方案:例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。

但是对于存储服务来说,是很难实现无状态的,所以我们只能通过冗余多个备份来解决单点问题。

如何做好动静分离?方案有哪些?

动态数据、静态数据

所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。两者的区别,就是看页面中输出的数据是否和URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。

如:一篇文章的内容就是静态数据,每个人看到的都一样。而我们看到的淘宝页面图片就是静态数据,因为首页的推荐是因人而异的,属于个性化的数据。

我们对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。

静态数据如何做缓存

  1. 把静态数据缓存到离用户最近的地方,如:用户浏览器里、CDN上、服务端的 Cache 中。
  2. 进行静态化改造:静态化改造就是,我们不仅缓存数据,还直接缓存HTTP连接。以键值对的形式保存,key为URL,value为数据。缓存在Web代理服务器上,这样一来,当请求来到代理服务器时,可以根据URL直接获取数据,就不用跑到真正的服务器中获取数据了。

热点数据的发现与处理

热点请求会占用大量的服务器资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占 90% 的服务器资源。

“热点”是什么

热点可分为两点:

  • 热点操作:如刷新页面、添加购物车、下单,在流量高峰期,都可以算是热点操作。
  • 热点数据:而热点数据,就是热点操作所操作的、获取的数据。其中,热点数据又可以分为两项。
    • 静态热点数据:即能够提前预知的热点数据。
    • 动态热点数据:即不能够提前预知的热点数据。

如何发现热点数据

1、发现静态热点数据

  • 方式一:可以通过商业手段,如让卖家通过报名参加的方式提前把热点商品筛选出来,具体实现方案,通过一个运营系统,把参加活动的商品数据进行打标,然后通过一个后台系统对这些热点商品进行预处理,如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题,即增加卖家的操作成本,而且实时性较差,也不灵活
  • 方式二:通过已有的大数据,进行大数据分析,如分析用户每天访问的、搜索的商品,然后统计Top N的商品,最终可以认为这些Top N的商品就是潜在的热点商品。

2、发现动态热点数据

静态热点数据的实时性较差,若我们能在秒级内完成热点数据的发现就更好了,这种情况下产生的热点数据,就是动态的。

“动态热点数据发现系统”的具体实现步骤:

  1. 构建一个异步系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如Nginx、缓存、RPC 服务框架等这些中间件。并且,一些中间件产品本身已经有了热点统计模块。
  2. 将上游系统收集的热点数据发送到热点服务台,然后下游系统就会知道哪些商品会被频繁调用,然后做热点保护。
    在这里插入图片描述
    如上图:我们可以依赖前面的导购页面提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中,然后进行数据分析,得到最终的热点数据,下游系统可以订阅这些热点数据,根据各自的需求进行不同的处理操作。

如何处理热点数据

处理热点数据通常有几种思路:一是优化,二是限制,三是隔离。

一、优化

常用的优化,其实就是做缓存。当然了,热点数据可能会实时变化的,它们是“临时”的热点,所以可以用一个LRU队列短暂地缓存。

二、限制

限制是一种保护机制,方案有很多,如:对商品ID进做一致性Hash,然后根据Hash分桶,每个分桶设置一个处理队列,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。

三、隔离

把热点数据隔离出来,单独存储,避免让 1% 的请求影响到另外的 99%,并且隔离出来后也更方便对这 1% 的请求做针对性优化

如何做好流量削峰?

为何要流量削峰

削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。

流量削峰有几种思路,下面介绍三种:排队、答题、分层过滤。

1、排队

常见的“排队方式”,即用消息队列缓冲瞬时流量,把同步的直接调用转换成异步的间接推送。但是,如果流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列同样也会被压垮,这样虽然保护了下游的系统,但是和直接把请求丢弃也没多大的区别。
在这里插入图片描述

2、答题

在这里插入图片描述

增加答题功能是为了增加购买过程的复杂度,从而达到两个目的:

  1. 防止买家使用“秒杀器”刷单。
  2. 延缓请求。

第一点就不用解释了,第二点“延缓请求”可以说一下,它就是把峰值的下单请求拉长,从以前的”1s之内就秒杀完了“变到”需要2s~10s才能秒杀完“。

这种”增加复杂度“的设计思路很普遍,如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式。

秒杀答题的设计思路

在这里插入图片描述
如上图所示,整个秒杀答题的逻辑主要分为三部分:

  1. 题库生成模块,题目和答案本身并不需要很复杂,重要的是能够防止由机器来算出结果,即防止秒杀器来答题
  2. 题库的推送模块,用于在秒杀答题,把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的,目的也是防止答题作弊。
  3. 题目的图片生成模块,用于把题目生成为图片格式,并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题,它要求只有人才能理解题目本身的含义。这里还要注意一点,由于答题时网络比较拥挤,我们应该把题目的图片提前推送到 CDN 上并且要进行预热,不然的话当用户请求题目时,图片可能加载比较慢,从而影响答题的体验。

3、分层过滤

分层过滤,即过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示。
在这里插入图片描述
像漏斗一样,每一层都进行一些判断从而过滤一些请求(即进行分层校验),尽量把数据量和请求量一层一层地过滤和减少,最后”漏斗“末端的请求才是有效请求

分层校验的基本原则

  1. 将动态请求的读数据缓存在 Web 端,过滤掉无效的数据读。
  2. 读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题。
  3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求。
  4. 对写请求做限流保护,将超出系统承载能力的请求过滤掉。
  5. 写数据进行强一致性校验,只保留最后有效的数据。

流量削峰小总结

  1. 队列缓冲方式很通用,它适用于内部上下游系统之间调用请求不平缓的场景。
  2. 答题更适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度。
  3. 分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候(写操作的时候)再做强一致性保证。

影响性能的因素

因素

举个例子:

  • CPU:看主频
  • 磁盘:看IOPS(每秒进行读写操作的次数)

对于服务端的性能,我们通常看QPS(每秒请求数)和RT(响应时间),RT又由两部分组成:CPU执行时间、线程等待时间。

  • 单线程情况下,RT越短,QPS也就越多,是一个线性的关系。
  • 但是在多线程情况下,总会有一个极限,“总 QPS =(1000ms / 响应时间)× 线程数量”。并且线程数不是越多越好的。我们的系统基本都是多线程的,那么可以说,影响性能的因素有”响应时间“和”线程数量“

如何发现瓶颈(秒杀场景)

即如何发现是哪些因素导致了性能上的瓶颈。就服务器而言,会出现瓶颈的地方有很多,例如 CPU、内存、磁盘以及网络等都可能会导致瓶颈。

需要注意的一点是,不同的系统对瓶颈的关注点也不一样,例如对缓存系统而言,制约它的是内存,而对存储型系统来说 I/O 更容易是瓶颈。

对于秒杀场景而言,基本都是在CPU上的瓶颈,那么如何发现呢?

使用工具

其实有很多 CPU 诊断工具可以发现 CPU 的消耗,最常用的就是 JProfiler 和 Yourkit 这两个工具,它们可以列出整个请求中每个函数的 CPU 执行时间,可以发现哪个函数消耗的 CPU 时间最多,以便你有针对性地做优化。

简单判断

当 QPS 达到极限时,看看你的服务器的 CPU 使用率是不是超过了 95%,如果没有超过,那么表示 CPU 还有提升的空间,即CPU目前不是瓶颈。(这个时候就要看看其他因素了,要么是有锁限制,要么是有过多的本地 I/O 等待发生…)

如何优化系统

对 Java 系统来说,可以优化的地方很多,下面简单介绍几种。

1、减少编码

在很多场景下,只要涉及字符串的操作(如输入输出操作、I/O 操作)都比较耗 CPU 资源,不管它是磁盘 I/O 还是网络 I/O,因为都需要将字符转换成字节,而这个转换必须编码。编码是需要查表的,很消耗资源,所以我们要减少编码。

案例:网页输出是可以直接进行流输出的,即用resp.getOutputStream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就可以减少静态数据的编码转换。

2、减少序列化

序列化往往是和编码同时发生的,所以减少序列化的同时也能减少编码。序列化大部分是在 RPC 中发生的,因此避免或者减少 RPC 就可以减少序列化,当然了,目前的序列化协议也已经做了很多优化来提升性能。有一种新的方案,就是可以将多个关联性比较强的应用进行“合并部署”,而减少不同应用之间的 RPC 也可以减少序列化的消耗。

所谓“合并部署”,就是把两个原本在不同机器上的不同应用合并部署到一台机器上,当然不仅仅是部署在一台机器上,还要在同一个 Tomcat 容器中,且不能走本机的 Socket,这样才能避免序列化的产生。

“减库存”设计

减库存方案

购买商品通常分为“下单”和“付款”两步,那么“减库存”操作应该在哪一步实现呢?通常来说,有下面几种实现方式:

  • 下单减库存
  • 付款减库存
  • 预扣库存

下单减库存

这是最简单,也是控制最精确的一种,下单时,直接通过数据库的事务操作控制库存的数量,一定避免了“超卖”的问题。

缺陷:有人下单之后却不付款,或者有人恶意刷单下单,直接把库存减为0了。

恶意下单解决方案:给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。

付款减库存

即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存。

缺陷:存在库存超卖的问题。假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。

库存超卖解决方案:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。

预扣库存

此方案相对复杂一些。

  1. 下单后,库存为其保留一定的时间,超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。
  2. 付款前,会检查该订单的库存是否还有保留:
    • 如果没有保留,则再次尝试预扣;
    • 如果预扣成功(库存充足)则完成付款并实际地减去库存;
    • 如果预扣失败(库存不足)则不允许继续付款;

缺陷:仍然存在被恶意下单的这种问题,假如库存的保留时间是10min,攻击者可以每隔10min就来再次下单。针对这种问题,要结合安全和反作弊的措施来制止。

大型秒杀中如何减库存

目前来看,业务系统中最常见的就是预扣库存方案,像你在买机票、买电影票时,下单后一般都有个“有效付款时间”,超过这个时间订单自动释放,这都是典型的预扣库存方案。

那对于秒杀场景的方案呢?由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用下单减库存更加合理。

并且,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。

如何保证库存不为负数

即保证数据库中的库存字段值不能为负数。

  • 通过事务来判断,保证减库存后不能为负数,否则回滚。
  • 直接设置数据库的字段为无符号整数,当其减库存为负数时会报错。
  • 使用CASE WHEN语句,如下:在这里插入图片描述

如何设计兜底方案

高可用建设应该从哪里着手

系统的高可用建设是一个系统工程,要在系统建设的整个生命周期上考虑。具体来说,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时

  • 架构阶段:考虑系统的可扩展性和容错性,要避免系统出现单点问题。
  • 编码阶段:保证代码的健壮性。如远程调用要设计超时机制,对调用结果集有预期,防止返回的结果超出了程序的处理范围。有全局异常捕获的能力,对无法预料的异常有默认处理。
  • 测试阶段:保证用例覆盖度,保证最坏情况下有预案。
  • 发布阶段:发布时最容易出现错误,因此要有紧急的回滚机制。
  • 运行阶段:运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
  • 故障发生:故障发生时首先最重要的就是及时止损,然后是恢复服务并定位故障位置。

针对秒杀系统的兜底方案

降级、限流、拒绝服务。

降级

所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。

例子:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
在这里插入图片描述
如上图所示,开关系统的示意图,它分为两部分,一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表。另一部分是执行下发开关数据的Agent,主要任务就是保证开关被正确执行。

降级是在“系统性能”和“用户体验”两者中,选择了前者。

限流

比降级更极端的保护是“限流”,即限制一部分流量来保护系统。限流可以从客户端进行限流,也可以在服务器上进行限流。并且,限流的方式不仅要支持URL级别的限流,还要支持QPS和线程数的限流。

在方式上,基于 QPS 和线程数的限流应用最多

  • 最大QPS数可以通过压测获知。
  • 线程数限流在客户端比较有效,例如在远程调用时我们设置连接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。
客户端限流
  • 优点:可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。
  • 缺点:客户端比较分散时,难以设置合理的限流阈值。如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制,如果设的太大,则起不到限制的作用。
服务端限流
  • 优点:方便根据服务端的性能设置合理的阈值。
  • 缺点:被限制的请求都是无效的请求,但是处理这些无效的请求本身也会消耗服务器资源。

拒绝服务

若使用了降级和限流仍不能解决问题,那就只能用最极端的方式了,即拒绝服务,这种方式是最暴力但也最有效的系统保护方式。

例子:在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码。在 Java 层同样也可以设计过载保护。

拒绝服务用于防止最坏的情况发生,避免服务器被压垮而导致长时间不能提供服务。虽然说,拒绝服务也是”不提供服务“,但是能保证服务器处于运行状态,以便于峰值过后的快速回复。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值