从全局角度,如何设计一个秒杀系统?
大家好,我是树哥。
秒杀系统的设计是高级职位面试中非常高频的一道题目,它可以较好地考察候选人的知识体系情况。对于我们来说,学习秒杀系统的设计,能够让我们学以致用,设计系统的时候考虑得更加全面。今天就让树哥带你一起来看看怎么设计一个秒杀系统!
活动一般出现在电商的促销活动中,一般是指定了很少数量的商品,以极低的价格,让大量的用户参与,从而造成大量用户在极短的时间内参与活动,进而造成系统在极短的时间内有极高的流量。系统设计的目的是使系统能够稳定地支撑活动的进行,因此其稳定性、高可用是我们考虑的第一位。
要知道如何进行秒杀系统的优化,那我们需要先对请求的整个流程有个全局的认识。一般来说,秒杀活动请求以公网为划分点,可以分为:前端部分、后端部分。 前端部分指的是从用户端到进入后端服务前的部分,包括了移动端的处理、DNS 解析、公网的数据传递等。后端部分指的是经公网进入了后端的服务器网络里,包括了前置的负载均衡(Nginx 等)、应用服务器、数据库层等。秒杀活动的整个流程可以用下图来表示。
我们要去设计一个秒杀系统,那自然也是从这两大部分来进行优化。整体思路是尽量将流量挡在前面,让尽量少的流量留到后端部分。因为越往后端,我们的处理逻辑就越重,其处理能力也越弱。
前端优化
对于前端部分来说,常见的优化手段有:页面静态化 + CDN、请求频率限制。
页面静态化 + CDN
一般来说,活动页面是流量最大的地方。活动页面上绝大部分内容都是固定的,比如:商品描述、图片等。这时候没有必要每次都去请求服务端,而是将这些静态的内容放到 CDN 上。每次打开页面的时候,直接去请求 CDN 服务器,能极大地减少后端的请求流量。加入了 CDN 之后,其请求过程如下:
所谓的 CDN 就是内容分发网络,它由非常多台分布在世界各地的缓存服务器组成。每次用户请求特定域名的时候,会转发到对应 CDN 的 DNS 解析服务器,随后会返回一台离用户地理位置最近的一台 CDN 服务器。随后,用户直接请求这台 CDN 服务器获取数据,从而极大地减少了长途网络传输的时间,并且也减少了后端服务器的压力。
因此,对于秒杀活动设计来说,我们可以将所有可以静态化的内容全部静态化,然后将其配置在 CDN 服务器上。这样既提高了用户打开页面的时间,又减少了后端服务器的压力。
请求频率限制
请求频率限制,指的是根据业务的特点,在前端做一些流量拦截,减少后端服务器的压力。常见的拦截方式有:
- 设定一个请求概率,只允许 30% 的概率向后端发送接口请求。
- 设定一个请求频率,例如 10 秒钟只能请求 1 次,随后按钮置灰。
通过这种方式,我们可以减少很大一部分流量。但在具体实现的时候,可能需要考虑安全问题,预防某些用户直接调用后台接口,绕过前端的频率检查。常见的方法是在频率检查时生成一个参数,随后请求后端服务时携带上该参数。没有该参数的请求,都视为非法请求,直接拒绝该请求。
后端优化
无论我们做多大的努力,始终还是会有不少流量会来到后端服务器这里。一般来说,后端的优化有如下几种方式:
- 增加缓存层 + 预热数据
- MQ 异步处理
- 限流、熔断、兜底
- 业务侧优化
增加缓存层 + 预热数据
如果我们所有数据都去读取数据库,数据库可能无法承受较大的流量,此时一个常见的优化就是增加缓存层。
当我们需要查询数据库之前,我们先去查询缓存,这样可以减少绝大部分的数据库请求,减轻数据库压力。如果在缓存中找不到数据,我们再去请求数据库,随后再将数据缓存到缓存中。在引入缓存层的时候,我们需要考虑缓存击穿、缓存穿透的可能性,在写相关代码的时候就要做好这些优化。
另外,我们在秒杀活动开始之前,可以手动将热点数据加载到缓存中,从而避免秒杀时去请求数据库。
MQ 异步处理
我们知道秒杀活动一般涉及抢购、下单、支付、发货等阶段,而抢购与后续的几个阶段是可以异步执行的。为了避免对下单、支付、发货等阶段产生影响,我们可以将抢购阶段与后续阶段用 MQ 进行解耦处理。当用户抢购成功后,往消息队列中丢入一台消息,随后再由订单系统消费进行下单处理。
通过各系统之间的解耦处理,我们可以将原本同步的处理方式变为异步处理,从而大大的减少了请求的处理时间,提高了系统的并发处理能力。其次,也能避免系统之间相互影响,提高了整体系统的稳定性。
限流、熔断、降级
虽然我们做了非常多的优化措施,但还是可能存在请求超量的可能性,那怎么办呢?
我们可以在每个业务系统做限流操作,从而避免因为请求太多,导致整个系统都无法工作。当并发请求在正常范围内时,我们正常处理请求。当超过设置的限流阈值时,我们则直接拒绝该请求,提示用户抢购失败。
如果没有限流操作,那么系统直接崩溃了,一个请求都处理不了。而通过限流这种方式,系统至少还可以保持正常工作,而不至于一个请求都处理不了。而超量的需求,本来就处理不了,因此提示失败也是情理之中。
除了限流之外,不同的系统还可以采用熔断、降级的服务治理措施。
熔断指的是请求的错误次数超过阈值时,不再到用后端服务,直接返回失败。同时每隔一定时间放几个请求去重试后端服务,看看是否正常。如果正常则关闭熔断状态,如果失败则继续快速失败。熔断的目的是避免因下游短暂的异常,导致上游不断重试,最终造成下游有太多请求,最终压垮下游系统。
降级指的是当服务失败或异常后,返回指定的默认信息。降级的目的是保证有基本的信息,当下游异常时,与其返回空信息,不如返回一个有业务含义的默认信息,可以提高用户体验。
业务侧优化
一般来说,经过上述的整体优化之后,系统已经能够比较稳当地应对秒杀活动了。如果此时还是流量比较大,那么或许应该从业务侧去进行优化了。
例如 12306 刚开始的时候,购买时间都在同一时刻,这导致同一时刻并发量太大,系统经常支撑不住。后来 12306 将购票周期放长,可以提前 20 天购买火车票。通过业务侧的优化,我们将本来在 1 个小时的抢购分摊到了 20 天,服务器压力一下子降低了 480 倍!
张小龙也说过:如果公司最厉害的程序员来实现业务都觉得复杂,那很可能就是业务确实不合理,这时候应该从业务侧进行优化。
例如一个存储了 10 亿条记录的消息记录表,业务侧既想查询速度快,又想进行 1 年数据范围的数据查询,这无论如何都是无法实现的。这时候就需要从业务需求侧进行优化,否则是无法两全其美的。对于这个场景,一个合理的实现方式是:要实现 1 年数据范围的查询,那么只能根据消息 ID 进行,因为这样可以使用上索引。而要根据时间范围进行查询,只能缩短查询时间到 3 天内,这样也可以满足业务需求。
因此从业务侧进行优化,是一个四两拨千斤的办法,可以极大地降低技术侧实现的难度。
总结
设计一个秒杀系统,整体而言可以从前端与后端进行优化。
对于前端优化而言,可以从「页面静态化 + CDN」、请求频率限制进行优化。
其中「页面静态化 + CDN」指的是将不变的静态数据固定下来,然后放入 CDN 服务器,从而降低用户请求的响应速度,降低服务器的并发压力。请求频率限制,则是通过抢购概率与抢购频率限制,降低后端服务器的服务压力。
对于后端优化而言,一般有「增加缓存层 + 预热数据」、「MQ 异步处理」、「限流、熔断、降级」、业务侧优化这 4 种优化方式。
其中「增加缓存层 + 预热数据」指的是将热点数据存入缓存,并在活动开始前提前加载到缓存中,降低数据库层的读取压力。「MQ 异步处理」指的是对于非必要的业务逻辑,通过 MQ 进行异步处理,降低请求处理延时,同时提高业务系统整体稳定性。
「限流、熔断、降级」是对于整体微服务的保护,其中限流指的是对请求进行限制,当超过限流阈值时,直接拒绝请求,保护系统本身;熔断指的是保护下游系统,当请求下游系统连续错误超过阈值时,自动不去请求下游系统,避免因重试流量过大击垮下游系统。降级指的是当请求失败时,自动返回默认数据,提高用户体验。业务侧优化,则是指从业务层面去进行逻辑优化,从而降低技术复杂度,使得业务与技术复杂度达到一个平衡的状态,有利于更好地实现秒杀系统的高可用与高并发。
上面说到的 6 个优化思路,是设计秒杀系统常见的优化思路。但在实际业务场景中,除了要保障正常的功能设计之外,还还考虑防刷、安全、黑产等问题,此时可能需要多考虑一些其他优化,例如:黄牛利用抢购工具抢购,导致正常用户无法抢到商品等。这时候可能需要考虑增加验证码,用 App 设备指纹等风控措施。此外,对于秒杀系统而言,做好业务指标和系统指标的埋点监控也是非常重要的。
参考资料
- VIP!极客时间!系统学习!如何设计一个秒杀系统
- 秒杀系统设计 - 掘金
- 秒杀系统怎么搞?虐死人......
- 架构之高并发:降级和熔断 | Java 全栈知识体系