车票
- 面试题1:你是怎么理解秒杀系统的?
- 面试题2:如果让你设计一个秒杀系统,你会怎么设计?
- 面试题3:你根据理解给我画一下秒杀的流程图吧
- 每日小结
本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识
、集合容器
、并发编程
、JVM
、Spring全家桶
、MyBatis等ORMapping框架
、MySQL数据库
、Redis缓存
、RabbitMQ消息队列
、Linux操作技巧
等。
面试题1:你是怎么理解秒杀系统的?
相信对你来说,秒杀
这个词肯定不陌生;从双十一购物到春节抢红包,再到 12306 抢火车票,“秒杀”的场景处处可见。简单来说,秒杀就是在同一个时刻有大量的请求争抢购买同一个商品并完成交易的过程
,用技术角度说就是大量的并发读
和并发写
。
如果你看过秒杀系统的流量监控图的话,你会发现秒杀就是那种瞬间流量很高,但是平时又没有流量的场景。就在秒杀开始那一秒是一个很高的峰
,这是因为秒杀请求在时间上高度集中于某一特定的时间点
。这就会导致一个特别高的流量峰值
,它对资源的消耗是瞬时的。(这里借张敖丙老哥的流量峰值图)
对秒杀这个场景来说,最终能够抢到商品的人数是固定的,比如秒杀活动有100台iphone13手机,那么100人和10000人发起请求的结果其实都是一样的
,只有100个是有效的请求而已
,并发度越高,无效请求也越多。
但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,但真正开始下单时,秒杀请求并不是越多越好。
因此我们需要设计一些规则,让并发的请求得到更多的缓冲,同时我们也要过滤掉一大批无效请求。
一、秒杀业务的典型特点:
- 瞬时流量大 ,参与用户多;
- 可秒杀商品数量少 ;
- 并发读远大于并发写;
- 秒杀状态的实时性更新要求高;
一次秒杀的流程可以分为三个阶段:
- 秒杀未开始
活动开始前,用户进入活动页,这个阶段有两种请求,一种是加载活动页信息,一个是查询活动状态
得到未开始的结果
, 一个用户进入页面两个请求各发起一次,这两种请求占比各半。
- 秒杀进行中
这个阶段持续时间非常短,看到抢购按钮(开始)
的用户大量发起秒杀请求,瞬时秒杀请求占比暴增,抗住这些秒杀请求就是秒杀系统是否能抗住高并发的关键。
- 秒杀结束
当商品被抢购完,进入结束状态,请求情况回到活动开始前。
秒杀系统主要围绕活动开始时间
和剩余库存
两个关键因素进行;坦白说其实贯穿整个活动的关键请求只有三种:加载活动页请求
、读取活动状态请求
、秒杀下单请求
。
秒杀的整体架构可以概括为稳
、准
、快
三个字,对应的分别是高可用
、数据一致性
、高性能
。
- 稳(高可用)
整个秒杀系统架构需要满足高可用,我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数)。QPS在预计水位线以下时要保证稳定,当QPS超出预期时也同样不能让系统挂掉
,要保证秒杀活动顺利完成,秒杀商品顺利地卖出去,这个是最基本的要求。
- 准(数据一致性)
准
就是保证数据的一致性,比如秒杀100台 iPhone13,那就只能成交100台,多一台少一台都不行。一旦库存出问题,平台就要承担损失,程序员就要被祭天。程序员:???
- 快(高性能)
高性能,请求速度要快。这就要求系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化。包括浏览器端
、CDN
、前端界面
、服务端负载
、服务端
、缓存端
、RPC
、请求队列
、DB端
等,每个地方都快一点,整个系统就完美了。
二、秒杀架构的几个原则
1、程序尽可能做得少
一方面是指在功能特性上有所为,有所不为;另一方面是指一次处理的信息量要少。接口负责的功能越少,读取信息量越少,速度越快
。
2、尽量将请求拦截在系统上游
传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时。秒杀中虽然流量很大,但实际下单的有效流量却很小
。你想100台iphone13手机,10万个人抢,其中有效请求也就那100条,请求有效率为0.1%。
3、读多写少的场景尽量用缓存
秒杀是典型的读多写少的应用场景,100台iphone13手机,10万个人抢,最多100个人下单成功,其他人其实都是到查库存这一步就没了,写比例占0.1%,读比例占99.9%
,缓存的典型使用场景
面试题2:如果让你设计一个秒杀系统,你会怎么设计?
秒杀和平时网购不同,参加秒杀活动的用户更关心的是如何能快速刷新商品秒杀页面
,在秒杀开始的时候抢先进入下单页面。而不是页面中那些花里胡哨的装饰和啥啥代金券,也不是商品详情写的多具体,图片P的多精美,因此秒杀系统的页面设计应尽可能简单。
商品页面中的抢购按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的
,不可以点击。
下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;
只有前100个提交的订单发送给网站的订单子系统
,其余用户提交订单后只能看到秒杀结束页面。
要做一个这样的秒杀系统,业务会分为两个阶段,
- 第一阶段是秒杀开始前某个时间到秒杀开始,这个阶段属于
准备阶段
; - 第二阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果,这个阶段为
秒杀阶段
。
假如请求分别经过 CDN、前端界面、后台服务和数据库这几层;我们通过分层过滤的方式,像漏斗一样把无效的请求一层层地过滤掉。分层过滤的核心思想就是:在不同的层次尽可能地过滤掉无效请求,让漏斗
最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验
。
一、前端层设计
首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时,在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。对于前端设计更多着眼于三方面:静态化
、限流
、扩容
。
- 静态化
将秒杀活动页面上的所有可以静态的元素全部静态化,尽量减少动态元素。但一个静态html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,在加上其他的css、js、图片等资源呢?因此我们要对静态数据做缓存。
我们要把静态数据缓存到离用户最近的地方。缓存到哪里呢?常见的有三种:用户浏览器里
、CDN
或者在服务端的Cache
中。统一把静态资源放到cdn节点上分散压力,由于CDN节点遍布全国各地,其实能缓冲掉绝大部分的压力。
CDN化部署有以下几个特点也可以说流程:
- 从CDN取出整个
静态页面
,缓存在用户浏览器中; - 如果强制刷新整个页面,也会请求 CDN,而不会给服务端造成压力;
- 实际有效请求,只是用户对秒杀开始后
抢购按钮
的点击。
这样就把90%以上的静态数据缓存在了用户端或者CDN上
,当真正秒杀时,用户只需要点击特殊的抢购按钮
按钮,而不需要刷新整个页面。这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据。
- 限流
另外我们需要在浏览器层做一些请求拦截工作:
(1)在html方面,按钮在开抢前一直置灰,到时间后由页面脚本刷新按钮为:可点击状态
;用户点击抢购按钮
发出请求后,按钮置灰,禁止用户重复提交请求;
对于一些好奇的程序员们,不喜欢点按钮,喜欢把按钮触发的链接拿出来,写个脚本抢是吧?如果你笑了,说明你丫干过~~但现在是行不通了,下单链接
在秒杀开始后会动态更新,才是真链接。从此程序员之间再没有信任可言。
(2)在JS方面,限制用户在x秒之内只能提交一次请求;针对一些恶意刷单的情况,可以把每个用户(IP)提交请求的次数记录到Redis中,写入一个标志位,避免被同一个IP重复抢单。对于那些专业刷单,拥有无数肉鸡(IP)、僵尸用户的情况呢?别想这么多,面试官谁会这么问,做个人吧。
僵尸用户刷单: 很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批
僵尸账号
,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的僵尸粉
的来源)。这种账号,使用在秒杀和抢购里,也是同一个道理。黄牛呗,现在可不能这么叫了,应该叫专业刷单人才。 魔高一尺,道高一丈。应对方式除了反爬虫那一套,还有通过弹验证码、答题校验等方式处理的。互相伤害呗,反正。
- 扩容
加机器,这是最简单的方法,通过增加前端池的整体承载量来抗峰值,这就不再赘述了。
二、服务层设计
服务层可能是我们后端开发比较在意的,毕竟是我们的活儿。对于秒杀系统来说我感觉涉及到了绝大部分并发编程的相关知识,如果经手过完整秒杀系统开发的同学,是幸运的,你已经积累到了程序生涯中最宝贵的经验之一。
对于技术架构方面其实可选的路子有很多,方向一致即可,咱们知道秒杀的主体思路就分层过滤
。像漏斗,过滤出最后那不到1%的有效请求,这些请求还是太多,就缓存、排队;压力还大就系统降级,放弃一些业务系统功能;还大就只能通过限流、拒绝策略挡住一些流量来保护系统可用。
服务端主要包括以下四个模块:
- 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上(负载均衡)。
- 用户请求预处理模块:判断商品
是不是还有剩余(库存)
来决定要不要处理该请求
,库存数据多存在Redis中,用于过滤出有效下单请求(Redis Lua
组合实现Redis事务原子性)。 - 用户请求处理模块:把通过预处理的请求(有效下单请求)封装成事务提交给数据库,并返回是否成功。
- 数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余库存数等。
- 缓存
对于并发读,你第一时间回想起谁?缓存!不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;
通过缓存限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,大概率有99.9%的请求被拦住了
。
- 排队
对于服务层,如果我清楚的知道只有100部手机,我放10w个请求去数据库有什么意义呢?对于写请求,做请求队列(流量削峰)
。
每次只放有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回已售完
;
- 降级
我们可以给系统进行分级,比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。
需要注意的是,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。
- 限流
限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,实现系统自动化保护。限流可以在客户端,也可以在服务端。
客户端限流,好处是可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点是当客户端比较分散时,没法设置合理的限流阈值,如果阈值设置的太小,会导致服务端没有达到瓶颈时客户端已经被限制,如果设置的太大,起不到限制的作用。
服务端限流,好处是可以根据服务端的性能设置合理的阈值,缺点是被限制的请求都是无效的请求,处理这些无效请求本身也会消耗服务器资源。
限流会影响用户的正常请求,也必然会导致一部分用户请求失败
,因此在系统处理这种异常时一定要设置超时时间
,防止因被限流的请求不能fast fail(快速失败)而拖垮系统。
- 拒绝策略
拒绝策略与服务间调用的熔断类似
,当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。拒绝服务用以防止最坏情况发生,防止因把服务器压垮而长时间彻底无法提供服务
,像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
面试题3:你根据理解给我画一下秒杀的流程图吧
流程图同上:
流程简述:
- 在秒杀前和秒杀结束后,用户请求到CDN的静态缓存,返回同一套静态页面数据;
- 秒杀开始后用户请求第一批打到Nginx上,通过负载均衡分发到分布式秒杀服务中;有些说此处再加请求队列,根据业务体量选择。
- 继续会先读Redis从节点获取库存数,库存为0直接返回false,大于0则改Redis主节点库存,Redis lua脚本可以实现事务原子性,实现Redis事务。
- 提供RPC接口供查询是否秒杀结束、剩余库存数等,通过队列形式生成订单并入库。
- 将生成的订单信息返回给服务端,进而返回到客户端或浏览器端。
- 秒杀结束。
秒杀系统设计中的知识点涵盖太多内容,本篇技术部分涉及较少,后面会通过2-3篇的篇幅来继续分享一下技术和业务层面的一些具体问题。深入研究后才发现,真的是你知道的越多,你不知道的越多。。。下面是下篇要写的一些秒杀常问问题,同学们有其他问题可以在评论区投稿,我来安排。 1、流量削峰具体该怎么做? 2、你们在业务流程中是怎么控制减库存的? 3、热点数据是怎么处理的? 4、怎么解决超卖和少卖问题的? 5、缓存失效的策略应该怎么定? 6、…
每日小结
正所谓十个面试九个秒杀。虽然是开玩笑,但通过我和粉丝们交流中发现,秒杀这个事儿在面试中真的越来越常见了。嗨,多的不说了,今天的内容你做到心中有数了么?对了,如果你的朋友也在准备面试,请将这个系列扔给他,如果他认真对待,肯定会感谢你的!!
好了,今天就到这里,学废了的同学,记得在评论区留言:打卡。
,给同学们以激励。