如何设计一个秒杀系统

16次阅读

共计 4571 个字符,预计需要花费 12 分钟才能阅读完成。

开篇词 | 秒杀系统架构设计都有哪些关键点?

  • 秒杀主要解决两个问题,一个是并发读,一个是并发写
  • 秒杀的整体架构需要做到:稳、准、快。

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

  • 架构原则:“4 要 1 不要”

    • 数据要尽量少
    • 请求数要尽量少
    • 路径要尽量短
    • 依赖要尽量少
    • 不要 有单点

架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。

02 | 如何才能做好动静分离?有哪些方案可选?

那到底什么才是动静分离呢?所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。

  • 简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。
  • 你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。你应该根据情况,把它们尽量缓存到离用户最近的地方。

如何做动静分离的改造

  • 我们如何把动态页面改造成适合缓存的静态页面呢?

    • URL 唯一。商品详情系天然 h 就 j 以做到 URL 唯一化,比如每个商品都由
      ID 来标识,那么 http://item.xxx.com/item.htm?… 就可以作为唯一的 URL 标识。
    • 分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
    • 分离时间因素。服务端输出的时间也通过动态请求获取。
    • 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
    • 去掉 Cookie,服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie.
  • 动态内容如何处理?

    • ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面В已一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
    • CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

03 | 有针对性地处理好系统的热点数据

  • 发现热点数据

    • 通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。
    • 通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
  • 怎么优化

    • 优化热点数据最有效的办法就是缓存热点数据,如果热点数据做了动静分离,那么可以长期缓存静态数据。但是,缓存热点数据更多的是 ” 临时”缓存,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用 LRU 淘汰算法替换。
    • 再来说说限制。限制更多的是一种保护机制,限制的办法也有很多,例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
    • 最后介绍一下隔离。秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让 1% 的请求影响另外的 99%,隔离出也更方便对这 1% 的请求做针对性的优化。

04 | 流量削峰这事应该怎么做?

对秒杀这个场景来说,最终能够抢到商品的人数是固定的,就说 100 人和 10000 人发起请求的结果都一样,并发度越高,无效请求也越多。

但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,但真正开始下单,秒请求并不是越多越好。因此我们可以设计一些规则,让并发的请求更多地延缓,而且我们甚至可以过滤掉一些无效请求。

  • 有损方案

    • ip 过滤,随机过滤,百分比过滤
  • 无损方案

    • 排队
    • 消息队列
    • 线程池加锁等待
    • 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)来恢复请求

秒杀系统中的常用削峰方法

  • 答题

    • 这个重要的功能就是把峰值的下单请求拉长,从以前的 1s 之内延长到 2s-10s。还能防止机器抢单。
  • 分层过滤

    • 对请求进行分层过滤,从而过滤掉一些无效的请求。
    • 浏览器层面:秒杀是否已经结束,答题是否正确
    • 缓存:商品状态是否正常,用户是否具有秒杀资格,库存判断
    • 数据:扣减库存

05 | 影响性能的因素有哪些?又该如何提高系统的性能?

  • 减少编码

    • 那么如何才能减少编码呢?例如,网页输出是可以直接进行流输出的,即用 resp.getOutputstream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就可以减少静态数据的编码转换。
  • 减少序列化

    • 序列化大部分是在 RPC 中发生的,因此避免或者减少 RPC 就可以减少序列化,当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案,就是可以将多个关联性比较强的应用进行 ” 合并部署 ”,而减少不同应用之间的 RPC 也可以减少序列化的消耗。
    • 所谓 ” 合并部署 ”,就是把两个原本在不同机器上的不同应用合并部署到一台机器上,当然不仅仅是部署在一台机器上,还要在同一个 Tomcat 容器中,且不能走本机的 Socket,这样才能避免序列化的产生。
  • 并发读优化

    • 需要划分成动态数据和静态数据分别进行处理:
      像商品中的“标题 ” 和 ” 描述 ” 这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;
    • 像库存这类动态数据,会采用 ” 被动失效 ” 的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。

06 | 秒杀系统“减库存”设计的核心逻辑

  • 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
  • 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付т了款的情况,因为可能商品已经被其他人买走了。
  • 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。

针对“库存超卖”这种情况,在 10 分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。

实际使用方案
  • 目前来看,业务系统中最常见就是预扣库存方案,像你在买机票、买电影票时,下单后一般都有个“有效付款时间”,超过这个时间订单自动释放,这都是典型的预扣库存方案。而具体到秒杀这个场景,应该采用哪种方案比较好呢?
  • 由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。
  • “下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:

    • 一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;
    • 另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;
    • 再有一就 用 CASE WHEN 判断语句,例如这样的 SQL 语句:

      • UPDATE item SET inventory= CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
      • 类似 update order set inventory = inventory – xxx where inventory >= xxx

07 | 准备 Plan B:如何设计兜底方案?

具体来说,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时。

  • 架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。
  • 编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。
  • 测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
  • 发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。
  • 运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
  • 故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及 B 下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。

针对秒杀系统,如何做到高可用?

  • 降级

    • 所谓 ” 降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
    • 降级方案可以这样设计:当秒杀流量达到 5w/ s 时,把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5 ″ 这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
  • 限流

    • 客户端限流,好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果的太大,则起т到限制的作用。
    • 服务端限流,好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。
  • 拒绝服务

    • 在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。
    • 拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
正文完
 0