通常来说,大厂开发的系统经常会遇到一些类似电商秒杀抢购、景点门票高并发抢购、特殊商品(比如口罩)高并发抢购、类似 12306 的高并发抢票类的系统。
背景
大家好,这篇文章给大家介绍一个非常经典的去大厂面试经常被问的一个问题,就是瞬时高并发抢购问题。
通常来说,大厂开发的系统经常会遇到一些类似电商秒杀抢购、景点门票高并发抢购、特殊商品(比如口罩)高并发抢购、类似 12306 的高并发抢票类的系统。
所以经常会问这一类高并发抢购类的问题,这个时候,小伙伴们如果不能有理有据的给出一整套高并发场景下系统可能遇到的各种问题,以及你对应的架构设计和解决方案,那基本面试可能就会凉掉。
所以今天就手把手带着大家来分析一下,假设在特殊物品库存紧缺的场景下,1 分钟内要抢购 10w 个口罩这类特殊物品,此时可能有数十万人这个量级瞬时涌入来进行抢购,这个时候系统可能会遇到哪些问题,我们应该如何来设计架构解决这类问题呢?
业务架构设计
首先在分析这一类问题的时候,我们先不要考虑这个瞬时高并发到底有多高,先得把实现购买这类特殊商品的一个基础业务架构图画出来,同时把业务流程分析清楚。
大家看下图,如果你要搞一个商品抢购的系统,肯定得有一个抢购系统,这个抢购系统你得依赖商品系统吧,毕竟抢购过程中需要对商品数据进行读写,你还得依赖库存系统进行库存扣减,同时你还得依赖价格系统来计算当前商品的购买价格,还得依赖营销系统来验证商品购买的优惠。
最后还得依赖鉴权认证、风控拦截类的基础系统来确定本次抢购是否可以执行,所以说,一次抢购涉及到的各种系统其实是很多的,完整的基础高并发抢购系统基础业务架构图。
如下图 1 所示:
图 1:高并发抢购系统业务架构设计
网络拓扑架构设计
另外的话,大家还得对你的抢购请求是如何一步一步到达你的抢购系统的,这个事情流程大家也是要画出来的。
一般来说,我们的 APP 移动端对后端访问都是通过一个域名来发起请求的,这个域名会经过 DNS 进行解析得到我们的 SLB 负载均衡系统的 ip 地址。
然后请求会发送到我们的 SLB 负载均衡系统上去,接着 SLB 负载均衡系统会把请求均匀分发给我们后端的 API 网关系统,然后 API 网关系统再把流量分发给我们的抢购系统。
所以大致如下图 2 所示:
图 2:高并发抢购网络拓扑架构设计
好的,当大家能当着面试官的面,麻溜儿的把上面那套业务架构图和生产部署网络拓扑图大致画出来以后,我们可以跟大家保证,虽然这个时候面试官看起来面无表情,但是心里的真实反映应该是这样的:小兄弟可以啊,一般人听到这个问题就直接懵逼了,这小子居然知道先从业务架构和网络拓扑架构入手进行分析。
但是大家别高兴的太早,距离你圆满的完成这个问题的分析,大致是才刚刚走完了西游记十万八千里中的八千里而已,剩下的十万还要继续走呢!这一路上大家马上要遇到各种妖魔鬼怪了!打起精神,接着一起来往下看。
秒杀业务流量洪峰
往往到这里,我们下一步应该分析的,就是日常流量和抢购流量的区别了,什么意思呢?
先来说说日常流量,这个意思就是说,平时没有抢购的时候,就是别人正常来买各种商品,系统的大致流量应该是每秒会有多少请求。
这个问题的话,不大好说,因为不同的公司其实是不太一样的,但是我们可以取一个较为中间的值,整个系统日常的话每秒也就 1000 次请求,这个是比较中肯的一个值,不高也不低。
如下图 3 所示:
图 3:日常并发抢购系统业务流量情况
一般来说,但凡你的抢购系统以及他依赖的每个系统部署在 2 台机器以上,每秒 1000 次请求这种常规流量,各个系统兄弟们同心协力,一起扛一抗,还是没太大问题的。
但是如果说搞这么一个活动,某个特殊商品,限量 10w 份,大家又特别需要他,然后呢,限定就是每天上午 10:00 开抢,每次都有几十万人眼睛放出红光盯着手机屏幕准备抢他,志在必得,这个时候,流量会搞成什么样子呢?
注意,重头戏来了,大体上来说,根据一般的抢购经验,往往你的 10w 件商品会在 1 分钟内抢光,而且根据二八法则,80% 的商品会在 20% 的时间内被抢光。
也就是说 8w 件商品可能会在 10s 内被抢购,而且参与抢购这 8w 件商品的流量达到了 80% 的人群数量,假设一共有 50w 人参与抢购,就是有 40w 人在 10s 内发起抢购请求,抢光了 8w 件商品。
这个时候,每秒的请求数量应该是 40w/10s = 4w/s 的 QPS,大家看下图 4:
图 4:高并发抢购系统业务流量情况
不知道大家看到上图是何感想?脑子别发蒙啊,面试官听得津津有味,咱们赶紧继续往下讲啊,不然你这时候停下来,你们会大眼瞪小眼的!那这个时候如果对你的抢购系统发起的请求量达到了每秒 4w,大家觉得会如何呢?
很简单,系统绝对会被打死,网络带宽打满、CPU 使用率达到 90% 多、数据库负载过高、下游依赖频繁超时,这一切问题都可能会发生,你要问为什么?
那就是因为你的系统常规化部署下,就是抗每秒 1000 的请求的,他们又不是设计来抗你每秒 4w 请求的。
架构设计优化
所以这个时候问题就牵扯到了一个点,那就是怎么才能让你的抢购系统可以抗下来每秒 4w 请求呢?
为了解决这个问题,就得趁着面试官打瞌睡的时候,咱兄弟偷偷给你传授一点武林秘籍了。
正常情况下,一台 4 核 8G 的机器,开 200 个线程处理请求,如果他要调用别的服务,或者是访问数据库,基本上每秒单台机器也就抗个 1000 的请求量。
并发抢购系统性能瓶颈分析
但是,注意,敲黑板划重点了,不是说你的 4 核 8G 机器就菜鸡到了只能抗每秒 1000 个请求,他的关键问题在于,他要调用别的服务,而且他还要访问数据库,就是因为这种通过网络去访问外部系统,才导致了他每秒抗的请求量比较菜鸡一些。
大家看下图 5:
图 5:并发抢购系统性能瓶颈
大家要知道一点,类似 Redis、RocketMQ 这种中间件系统,经过深度优化之后,往往单台抗个上万甚至几万 QPS 都没问题,所谓的深度优化是什么意思?
简而言之就一点,你最好就是每次请求过来,完全就基于自己的内存来读写数据,然后就直接返回了。
不要随便通过网络去访问外部的系统,这种情况下,往往你的并发量可以提升几个数量级。
如下图 6 所示:
图 6:并发抢购系统架构深度优化
并发抢购系统架构优化
所以说,一般这种场景下,有三个非常强悍的优化手段,那就是大幅度减少对外部服务的依赖调用吗;写数据尽量直接写缓存,然后异步写 DB;读数据尽量优先把数据缓存在系统 JVM 内存里,本地读取返回。
这里可以给大家举一些例子,比如说,对于特殊商品固定价格抢购,那么对价格系统、营销系统的调用是否就可以省略了,毕竟价格固定,也没有优惠这一说。
对于风控和鉴权类的通用操作,是否可以前置到 API 网关层面让他去执行,从我们的业务系统里移除这类通用逻辑?这不就一下子减少了对 4 个系统的调用了。
再比如说,对库存的扣减,是否可以让库存系统把数据同步到 Redis 里,我们直接同步扣 Redis 里的库存,然后发 MQ 消息异步去库存系统的 DB 里扣库存?
还有比如对商品数据的大量查询,是否可以将商品数据缓存到 Redis 里,同时对热门商品数据全部提前加载到抢购系统的 JVM 内存里本地缓存?
经过优化后的抢购系统大致看起来是下面图 7 这样子的:
图 7:并发抢购系统架构缓存优化
大家看上图,这个时候经过一通优化之后,我们的抢购系统已经不再直接调用任何服务了。
他在读商品数据的时候,优先都是从自己的 JVM 本地缓存里读取预缓存的数据,几乎就是纯内存操作,然后扣减库存是去写 Redis 的,对于库存系统甚至是订单系统的数据库中的扣减库存和下单,都是通过 MQ 异步化执行的。
基本上系统优化到这个水准,主要给抢购系统多部署几台机器,就可以抗下每秒几万高并发的请求了。
但是这个时候完了吗?当然没有,这个时候系统里存在的问题还非常的多,我们得继续往下分析,进一步一步一步的优化。
①高并发抢购系统缓存击穿问题分析与解决方案
首先,分析第一个问题,就是商品数据缓存在抢购系统 JVM 本地缓存时的击穿问题,我们在抢购系统的 JVM 本地缓存中放的数据,一般都是要设置一个过期时间的,因为如果你一直缓存在 JVM 里,会导致商品数据有变化了,你也不知道。
所以假设我们设置一个 30min 的过期时间,每隔 30min 过期下,过期之后,抢购系统就得去 Redis 里查商品数据缓存,如果没查到,那就得去调用商品系统的接口从数据库里查了。
如下图 8:
图 8:高并发抢购系统 — 缓存数据过期问题
那么当你的抢购系统里的本地缓存过期了,此时本地缓存没数据了,然后 Redis 里缓存可能此时也没有的时候,就在这个非常要紧的关头,偏偏就进来了大量的请求,此时这大量请求在本地缓存都没找到,去 Redis 里也没找到,然后呢?
然后当然就是完犊子了,因为这些请求都会涌入到商品系统里去,让商品系统从数据库里查询,直接把商品系统击穿。
如下图 9:
图 9:高并发抢购系统 — 缓存击穿问题
所以这个时候,我们往往需要对这种本地缓存做一个特殊的方案设计,那就是对于本地缓存不要采取这种让他自动过期然后请求过来的时候读取不到再去商品系统那里查找的模式,而是采取抢购系统针对本地缓存自动定时刷新。
也就是说,抢购系统内可以开一个后台线程,然后让他每隔 30min 自动去 Redis 里查最新缓存数据,或者去商品系统查最新缓存数据,然后刷新本地缓存,这样就可以避免说自动过期后突然大量请求查不到缓存都涌入商品系统了。
如下图 10:
图 10:高并发抢购系统 — 缓存自动刷新机制
②高并发抢购系统数据不一致问题分析与解决方案
再来看下一个比较常见的问题,就是扣库存的缓存与 DB 不一致问题,这个问题的场景可能发生在如下情况。
就是说你在 Redis 里扣完了库存之后,通过 MQ 发送了一个消息异步让那个库存系统在 DB 里扣库存,可是人家库存系统还没在 DB 里扣减呢,这个时候你突然因为异常回滚了这次库存扣减,此时 Redis 里把扣的库存恢复了,然后发了一个消息到 MQ 去恢复库存扣减。
如下图 11:
图 11:高并发抢购系统 — 数据不一致问题(一)
但是这个时候 Redis 里的库存是恢复了,可是库存系统 DB 那里就是未必了,因为库存系统从 MQ 里获取消息的时候,很有可能是乱序获取的,就是先获取到恢复库存的消息。
此时库存系统一般会判断一下,之前是否对这次抢购有过库存扣减日志,如果没有,他就不会去恢复库存,然后接着再获取到扣减库存的消息,此时他就扣减了库存,可是恢复库存的消息再也没机会处理了。
如下图 12:
图 12:高并发抢购系统 — 数据不一致问题(二)
那么上面会导致什么呢?会导致 Redis 里扣减了库存,又恢复了库存,可是库存系统的 DB 里先获取了恢复库存指令,结果什么都没干,然后又获取了扣减库存指令,反而把库存给扣了,此时缓存和 DB 里的库存是不一致的。
所以针对这个问题,通常都会实现 MQ 顺序消息,也就是说,把同一个抢购订单的多个库存操作指令发送到 MQ 的一个分区里去,让他们实现有序,强制要求库存系统必须按照顺序依次获取后执行,这样就会先执行扣减库存指令,再执行恢复库存指令了。
如下图 13:
图 13:高并发抢购系统 — MQ 顺序消息
总结
好了,今天这篇文章到这里为止,就给大家讲了一下大厂里我们经常遇到的高并发抢购类系统的架构设计和优化过程,以及缓存击穿与数据乱序不一致问题的分析和解决方案。
希望大家在阅读后能在未来面试遇到这类问题的时候,有理有据的逐步分析逐步展开,让面试官看到大家沉稳如水、细致如丝的应变能力。