后端进阶 每一步成长都想与你分享

面试官问我:如何设计一个秒杀场景?

2021-05-19
张乘辉

前段时间在公众号读者交流群,有读者提问到关于并发场景相关的问题:

从读者的描述,可以看出高并发处理的经验,在面试中占据着举足轻重的地位,关于高并发相关的面试题,一直都是面试热题,因为这类面试题能够更加直观地体现候选人的技术水平与深度。如何解决高并发场景下的问题,永远都不会过时。

在之前的工作经历中,我做过营销相关项目,接触过关于票券秒杀的高并发场景,秒杀场景也算是最热门的高并发场景之一了。

下面我就把我对秒杀场景的一些理解简单写下来,仅供大家参考,欢迎留言纠错或者补充。

核心要素

何为高并发?

高并发指的是在同一时刻,有大量用户的请求同时到达服务器,而服务器需要在有限的资源内处理这些请求,并尽可能快地响应用户请求。

在秒杀场景中,我们需要从在大量并发请求过程中提升服务器的处理性能,在处理过程中数据处理不能存错,同时在整个秒杀链路中需要满足高可用性,即在秒杀过程中,服务不能突然掉链子,需要满足秒杀场景活动生命周期的完成。

我们可以总结出秒杀场景中有三个核心要素:

  1. 高性能;
  2. 一致性;
  3. 高可用性。

如何提高性能?

秒杀场景核心的问题是如何解决海量请求带来的性能问题,那么我们如何在有限的资源下,尽最大的限度去提高服务器访问性能?按照我以往的经验,我大致总结有这几点:热点数据处理、流量削峰、资源隔离、服务器优化。

热点数据处理

1、什么是热点数据?

我理解的热点数据指的是用户请求量非常高的那些数据,在秒杀场景中,热点数据就是那些要被秒杀的商品数据。

这些热点请求会大量占用服务器的资源,如果不对这些数据进行处理,那么会严重占用资源,进而影响系统的性能,导致其他业务也受影响。

热点数据又可以分为“静态热点数据”和“动态热点数据”。

2、静态热点数据

静态热点数据指的是可以提前预知的热点数据,比如本文所说的秒杀场景,需要参与本次秒杀的商家提前报名,并将秒杀的商品录入热点分析系统中。业务系统通过这次提前录入的热点数据,进行预加载,甚至可以将数据放入本地缓存中,这样做的好处可以有效缓解避缓存集群的压力,避免流量集中时压垮缓存集群。

可能有人会问如何更新本地缓存?

我的做法是将热点数据录入热点分析平台,本地对热点数据进行订阅,并根据订阅规则去更新本地缓存即可。

3、动态热点数据

动态指的就是不能提前预知哪些数据是热点的,需要通过数据收集与分析,或者通过大数据平台预测。

我的做法是通过在网关平台中做一个用于收集日志的异步日志收集系统,通过采集商品请求的日志,处理后发送到热点分析平台,热点分析平台通过一些列的分析计算将这些热点商品进行热点数据处理,后端通过订阅这些热点数据就可以识别哪些商品是热点数据了。

流量削峰

在服务器资源固定的情况下,说明处理能力是有峰值存在的,如果不对请求处理进行处理的话,很可能会在流量峰值的瞬间压垮服务器,但流量峰值存在的时间不长,其实服务器的处理能力大部分时间都是处于闲置状态,那么我们可不可以将峰值集中的请求分散到其他时间呢?

1、消息队列

消息队列除了在解耦、异步场景之外,最大的作用场景是用于流量削峰,面对海量流量请求,可以将这些请求数据用异步的方式先存放在消息队列中,而消息队列一般都能够存储大量消息,消息会被消费端订阅消费,这样就有效地将峰值均摊到其他时间进行处理了。

如上,消息队列就像我们平常见到的水库一样,当洪水来临时,拦住并对其进行储蓄,以减少对下游的冲击,避免了洪水的灾害。

目前有大量优秀的开源消息队列框架,如 RocketMQ、Kafka 等,而我之前在中通时主要负责消息平台的建设与维护工作,中通每天面对几千万的订单流量依然那么稳固,其中消息队列起了很大的“防洪”作用!

2、答题

除了利用消息队列对请求进行“储蓄”达到削峰的目的之外,还可以通过在用户发起请求前,对用户进行一些校验操作,比如答题、输入验证码等等,这种答题机制,除了可以防止买家在秒杀过程中使用作弊脚本之外,在秒杀场景中最主要的作还是将请求分散到各个时间点,秒杀场景一般都是集中在某个点进行,比如 0 点时刻,如果没有答题机制,几乎所有的流量都在 0 点时刻涌入服务器中,如果有答题机制,就能延缓用户的请求,从而达到请求分散到各个时间点的目的。

如何保持一致性?

秒杀场景,本质上就是在海量买家同时请求购买时,能够准确并将商品卖出去。

在秒杀的高并发读写请求过程中,需要保证商品不会发生“超卖”现象,因为秒杀的商品是数量一定的,但会有成千上万个用户在同一时间下单购买,在减扣库存过程中如何保证商品数量的准确性至关重要。

减扣库存方案分析

我在以前在做秒杀项目的时,分析过几种减扣库存的方式,我简单分析下。

1、下单减扣库存

买家只要完成下单,立即减扣商品库存,这种方式实现是最简单而且也是最精准的,通常可以在下单时利用数据库事务能力即可保证减扣库存的准确性,但需要考虑买家下单后不付款的情况。

2、付款减扣库存

即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。

当只有买家下单后,并且已完成付款,才执行库存的减扣,这种方式好处是避免了买家不付款导致实际没有卖出这么多商品的情况,但这种方式会造成用户体验不好,因为这会导致有些用户付款时商品有可能被人买走了导致付款失败的问题。

3、预扣库存

这种方式结合以上两种方式的优点,当买家下单后,预扣库存,只会其保留一定的时间,比如 10 分钟,在这段时间内如果买家不付款,则将库存自动释放,其它买家可以继续抢购。这种做法需要买家付款前,再做一次商品库是否还有保留,如果没有保留,则再次尝试预扣,预扣失败则不允许继续付款;如果有保留,付款完成后执行真正的减扣库存动作。

但预扣库存依然没有彻底解决减扣库存链路中存在的问题,比如有些买家可以在释放的瞬间立马又重新下单一次,相当于将库存无限地保留下去,因此我们还需要将记录用户下单次数,如果连续下单超过一定次数,或者超过下单并不付款次数,就拦截用户下单请求。

总结:

一般最简单的做法就是使用下单减库存的方式(我之前的项目中就是用的这种),我当初的考虑是因为在秒杀场景中,商品的性价比通常很高,秒杀就是创造一种只有少量买家能买到的场景,一般来说买家只要“秒”到商品了,极少情况会出现退款的,即使发生了少量退款,造成实际卖出去的商品会比数据上少,也是可以通过候补来解决。

如何减扣库存?

减扣库存动作应该放在哪里执行?

下面我具体分析一下减扣库存的几种实现方式:

  1. 如果链路涉及的逻辑比较简单的,比如下单减库存这种方式,最简单的做法就是在下单时,利用数据库的本地事务机制进行对库存的减扣,比如使用 where 库存 >0 不满足就回滚;
  2. 将库存数量值放在缓存中,比如 Redis,并做持久化处理。

需要注意的是,如果遇到减扣库存的逻辑很复杂,比如减扣库存之后需要在同一个事务中做一些其他事情,那么就不能使用第二种方式了,只能使用第一种方式在数据库层面上面操作,以保证同在一个事务中。面对这种情况,你可以将热点数据进行数据库隔离,把这些热点商品单独放在一个数据库中。

如何实现高可用性?

最后,为了保证秒杀系统的高可用性,必须要对系统进行兜底处理,以便遇到极端的情况系统依然能够运转,通常的做法有服务降级、服务限流、拒绝请求等方式处理。

服务降级

当请求量达到系统承受的能力时,需要对系统的一些非核心功能进行关闭操作,尽可能将资源留给秒杀核心链路。

比如在秒杀系统中,还存在其他非核心的功能,我们可以在系统中设计一些动态开关,比如在网关层在路由开关,将这些非核心的请求直接在最外层拒掉。

还有就是对页面展示的数据进行精简化,用降低用户体验换取核心链路的稳定运行。

服务限流

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,常用的有 QPS 限流,用户请求排队限流,需要设置过期时间,一旦超过过期时间则丢弃,这样做是为了用户请求可以做到快速失败的效果,这种机制在 RocketMQ 中也有相关的应用,RocketMQ broker 会对客户端请求进行排队限流处理,当请求在队列中超过了过期时间,则丢弃,客户端快速失败进行第二轮重试。

拒绝请求

如果服务降级、服务限流都不能解决问题,最后的兜底,那就是直接拒绝用户请求,比如直接给用户返回 “服务器繁忙,请稍后再试”等提示文案。只会发生在服务器负载过载时会启动,因此只会发生短暂不可用时刻,由于此时服务依然还在稳定运行中,等负载下降时,可以快速恢复正常服务。


更多精彩文章请关注作者维护的公众号「后端进阶」,这是一个专注后端相关技术的公众号。 关注公众号并回复「后端」免费领取后端相关电子书籍。 欢迎分享,转载请保留出处。

微信公众号「Java科代表」

Content