本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
秒杀服务即使自己扛不住高并发而宕机,也不要造成服务雪崩。
秒杀读多写少。无需实时校验库存。库存预热,放到Redis,信号量控制进来秒杀的请求。
Nginx做好动静分离。静态资源 Nginx 直接返回,保证秒杀和商品详情页的动态请求才打到后端服务集群。
使用CDN网络,分担本集群的压力。
服务网关识别非法攻击请求并进行拦截。
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车。
前端限流+后端限流 限制次数,限制总量,快速失败降级运行, 熔断隔离防止雪崩。
1万个商品,每个1000件秒杀。双11 所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。
不用分布式锁,直接将商品根据id创建一个队列,让用户从队列中取,通过自增后是否大于0来防止超卖问题,并发度和Redis分布式锁效率比,哪个好?
队列实现秒杀系统,无需分布式锁,因每个商品对应自己的队列,队列中商品出队即可预防超卖。但并发度和Redisson分布式锁比较,需结合实际选型:
Q:用这个方案,秒杀过程中想添加库存,数据库库存变多,咋将这消息发给redis让它添加库存?
A:可用Redis PUBLISH/SUBSCRIBE功能,把增加库存的消息发布到特定channel,再实时订阅该channel的服务接收到消息并处理。服务可在收到消息后用incrby增加库存量。这可使库存变动更及时可靠,也更适合对对库存变动要求高的场景。
高并发系统设计的三个目标:性能、可用性和可扩展性。
在提升系统性能方面我们一直关注的是系统的查询性能,比如数据库的分布式改造,各类缓存。因为大部分场景都是读多写少。
比如一个社区系统初期一定是只有少量的种子用户在生产内容,而大部分的用户都在“围观”别人在说什么。此时,整体流量较小,而写流量可能只占整体流量的百分之一,那么即使整体的QPS到了1w,写请求QPS也只是到了100,如果要对写请求做性能优化,性价比不高。
但随着业务发展,可能遇到一些存在高并发写请求场景,比如秒杀。假设你的商城策划了一期秒杀活动,活动在第五天的00:00开始,仅限前200名,那么秒杀即将开始时,后台会显示用户正在疯狂地刷新APP或者浏览器来保证自己能够尽量早的看到商品。
但读请求依旧过高,如何应对?
最早期,量太大扛不住,直接前端随机reject一些,返回抢单失败,简单粗暴,但有效,比如10万人抢100个iPhone,只要能提前预测有大概1万以上的人参与(通过资格确认、报名等方式收集信息),那么直接请求进来以后随机挡回去99%的流量都没有啥问题。
中间有段时间,提前准备一大批机器,服务化、分库分表搞定后端性能,让前端业务可以加一定量的机器,然后搞稳定性,依赖关系,容量规划,做弹性,提升吞吐量。
使用可堆积的消息队列或内存消息队列。若抢单具有强顺序,则先都进队列,然后拿前N (就是库存数)个出来平滑处理,剩下都可作为失败进行批处理。
甚至还可做一个定长队列,再往里写直接提示失败。队列把并发变成串行,从而去掉了分布式锁。
某些业务可以考虑预热,提前在每个机器节点内存分配好库存数,然后直接在内存处理库存数。
对于不同类型、不同商家、不同来源的商品,部署不同的前端促销集群,
分散压力。比如,按每个整点发起秒杀,具体到每个商家,其实量就不大了。
越重要的抢单,大家越关心自己有没有抢到,而不是特别在意订单立即处
理完,也就是说,下单占到位置比处理完成订单要更有价值。如12306春运抢票,只要告诉用户你抢到票,但预计1h后订单才会处理完,用户有这个明确预期即可。用户不会立马用这张票,不会在意1min还是1h处理完。
部分方案会导致少卖或超卖:
因为用户查询的是少量商品数据,属查询热点,可用缓存将请求尽量挡住,能被静态化的数据(如商城里的图片和视频数据)尽量静态化,即可命中CDN节点缓存,减少Web服务器的查询量和带宽负担。Web服务器如Nginx可直接访问分布式缓存节点,避免请求到达Tomcat等业务服务器。
也可加限流,如对短时间之内来自某一个用户、某一个IP或者某一台设备的重复请求做丢弃处理。
通过这几种方式,请求尽量挡在DB前。
稍微缓解了读请求之后,00:00分秒杀活动准时开始,用户瞬间向电商系统请求生成订单,扣减库存,用户的这些写操作都是不经过缓存直达数据库的。1秒钟之内,有1万个数据库连接同时达到,系统的数据库濒临崩溃,寻找能够应对如此高并发的写请求方案迫在眉睫。这时你想到MQ。
把消息队列看作暂时存储数据的一个容器,它是一个平衡低速系统和高速系统处理任务时间差的工具。
比如古代臣子朝见皇上陈述国家大事,等皇上决策。但大臣很多,如果同时去找皇上,皇上肯定会崩溃。后来变成臣子到午门后要原地等皇上将他们一个一个地召见进大殿商议,这就缓解皇上处理事情的压力。
可以把午门看作一个暂时容纳臣子的容器,即消息队列:
那如何用消息队列解决秒杀场景下的问题呢?
在秒杀场景下短时间之内数据库的写流量很高,按以前思路,应该分库分表。若已分库分表,则需扩展更多数据库应对更高写流量。
但无论分库分表or扩充更多数据库都会很复杂,因为你需要迁移数据库中的数据,这个时间就要按天甚至周计算。
而在秒杀场景下高并发的写请求并不是持续的,也不是经常发生,而只有在秒杀活动开始后的几s或十几s时间内才存在。
为了应对这十几s瞬间写高峰,而去花费几天甚至几周扩容DB,再在秒杀之后花费几天做缩容,得不偿失!
所以思路是:将秒杀请求暂存在MQ,然后业务服务器会响应用户“秒杀结果正在计算”,释放了系统资源之后再处理其它用户请求。
在后台启动若干个队列处理程序消费MQ中的消息,再执行校验库存、下单等逻辑。因为只有有限个队列处理线程在执行,所以落入后端DB上的并发请求有限。而请求是可以在MQ被短暂堆积,当库存被消耗完后,消息队列中堆积的请求就可以被丢弃了。
这就是MQ在秒杀系统中主要作用:
可削平短暂流量高峰,虽说堆积会造成请求被短暂延迟处理,但只要我们时刻监控MQ中的堆积长度,在堆积量超过一定量时,增加队列处理机数量来提升消息处理能力即可,而且秒杀用户对于短暂延迟知晓秒杀的结果也有一定容忍度。
注意是“短暂”延迟,若长时间没有给用户公示秒杀结果,则用户会怀疑秒杀活动有黑幕。所以在使用MQ应对流量峰值时,需要对队列处理的时间、前端写入流量的大小、数据库处理能力做好评估,然后根据不同的量级来决定部署多少台队列处理程序。
比如你的秒杀商品有1000件,处理一次购买请求的时间是500ms,那么总共就需要500s的时间。这时你部署10个队列处理程序,那么秒杀请求的处理时间就是50s,也就是说用户需要等待50s才可以看到秒杀的结果,这是可以接受的。这时会并发10个请求到达数据库,并不会对数据库造成很大的压力。
其实在大量的写请求“攻击”你的电商系统的时候,消息队列除了发挥主要的削峰填谷的作用之外,还可以实现异步处理来简化秒杀请求中的业务流程,提升系统的性能。
你想,在刚才提到的秒杀场景下,我们在处理购买请求时需要500ms。这时你分析了一下整个的购买流程,发现这里面会有主要的业务逻辑,也会有次要的业务逻辑:比如说,主要的流程是生成订单、扣减库存;次要的流程可能是我们在下单购买成功之后会给用户发放优惠券,会增加用户的积分。
假如发放优惠券的耗时是50ms,增加用户积分的耗时也是50ms,那么如果我们将发放优惠券、增加积分的操作放在另外一个队列处理机中执行,那么整个流程就缩短到了400ms,性能提升了20%,处理这1000件商品的时间就变成了400s。如果我们还是希望能在50s之内看到秒杀结果的话,只需要部署8个队列程序就好了。
经过将一些业务流程异步处理之后,我们的秒杀系统部署结构也会有所改变:
除了异步处理和削峰填谷以外,消息队列在秒杀系统中起到的另一个作用是解耦合。
比如数据团队对你说,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度、购买者人群的特点以及用户对于秒杀互动的满意程度等等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢?
用HTTP或RPC来同步调用,即数据团队提供一个接口,我们实时将秒杀数据推给它,但这样调用会有问题:
可考虑MQ降低业务系统和数据系统的直接耦合度。
秒杀系统产生一条购买数据后,我们可以先把全部数据发送给消息队列,然后数据团队再订阅这个消息队列的话题,这样它们就可以接收到数据,然后再做过滤和处理了。
秒杀系统在这样解耦合之后,数据系统的故障就不会影响到秒杀系统了,同时当数据系统需要新的字段时,只需要解析消息队列中的消息,拿到需要的数据就好了。
异步处理、解耦合和削峰填谷是消息队列在秒杀系统设计中起到的主要作用,其中异步处理可以简化业务流程中的步骤,提升系统性能;削峰填谷可以削去到达秒杀系统的峰值流量,让业务逻辑的处理更加缓和;解耦合可以将秒杀系统和数据系统解耦开,这样两个系统的任何变更都不会影响到另一个系统,
如果你的系统想要提升写入性能实现系统的低耦合,想要抵挡高并发的写流量,那么你就可以考虑使用消息队列来完成。
削峰填谷,但会造成请求处理的延迟。
异步处理是提升系统性能的神器,但要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,我们需要考虑如何确保消息一定到达。解耦可提升你的整体系统的鲁棒性。
使用MQ后,虽能解决现有问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发?这些问题都是我们需要考虑的。