如何设计一个高可用高并发秒杀系统
Posted 腾讯技术工程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何设计一个高可用高并发秒杀系统相关的知识,希望对你有一定的参考价值。
作者:vincentsu,腾讯 PCG 后台开发工程师
如今的互联网已经在海量服务领域有了很成熟的理论,因此自己也很庆幸,能够从 0 到 1 完整践行海量服务。微视春节项目中的集卡瓜分活动,是一个典型的秒杀场景,自己参与其中,分享一些心得和总结。
如今的互联网已经在海量服务领域有了很成熟的理论,因此自己也很庆幸,能够从 0 到 1 完整践行海量服务。微视春节项目中的集卡瓜分活动,是一个典型的秒杀场景,自己参与其中,分享一些心得和总结。
秒杀系统的难点
友好的用户体验
用户不能接受破窗的体验,例如:系统超时、系统错误的提示,或者直接 404 页面
瞬时高并发流量的挑战
木桶短板理论,整个系统的瓶颈往往都在 DB,如何设计出高并发、高可用系统?
上图是一个典型的互联网业务,用户完成一个写操作,一般会通过接入层和逻辑层,这里的服务都是无状态,可以通过平行拓展去解决高并发的问题;到了 db 层,必须要落到介质中,可以是磁盘/ssd/内存,如果出现 key 的冲突,会有一些并发控制技术,例如 cas/加锁/串行排队等。
直筒型
直筒型业务,指的是用户请求 1:1 的洞穿到 db 层,如下图所示。在比较简单的业务中,才会采用这个模型。随着业务规模复杂度上来,一定会有 db 和逻辑层分离、逻辑层和接入层分离。
漏斗型
漏斗型业务,指的是,用户的请求,从客户端到 db 层,层层递减,递减的程度视业务而定。例如当 10w 人去抢 1 个物品时,db 层的请求在个位数量级,这就是比较理想的模型。如下图所示
这个模型,是高并发的基础,翻译一下就是下面这些:
及早发现,及早拒绝
Fast Fail
前端保护后端
如何实现漏斗型系统
漏斗型系统需要从产品策略/客户端/接入层/逻辑层/DB 层全方位立体的设计。
产品策略
轻重逻辑分离,以秒杀为例,将抢到和到账分开;
抢到,是比较轻的操作,库存扣成功后,就可以成功了
到账,是比较重的操作,需要涉及到到事务操作
用户分流,以整点秒杀活动为例,在 1 分钟内,陆续对用户放开入口,将所有用户请求打散在 60s 内,请求就可以降一个数量级
页面简化,在秒杀开始的时候,需要简化页面展示,该时刻只保留和秒杀相关的功能。例如,秒杀开始的时候,页面可以不展示推荐的商品。
客户端
重试策略非常关键,如果用户秒杀失败了,频繁重试,会加剧后端的雪崩。如何重试呢?根据后端返回码的约定,有两种方法:
不允许重试错误,此时 ui 和文案都需要有一个提示。同时不允许重试
可重试错误,需要策略重试,例如二进制退避法。同时文案和 ui 需要提示。
ui 和文案,秒杀开始前后,用户的所有异常都需要有精心设计的 ui 和文案提示。例如:【当前活动太火爆,请稍后再重试】【你的货物堵在路上,请稍后查看】等
前端随机丢弃请求可以作为降级方案,当用户流量远远大于系统容量时,人工下发随机丢弃标记,用户本地客户端开始随机丢弃请求。
接入层
所有请求需要鉴权,校验合法身份
如果是长链接的服务,鉴权粒度可以在 session 级别;如果是短链接业务,需要应对这种高并发流量,例如 cache 等
根据后端系统容量,需要一个全局的限流功能,通常有两种做法:
设置好 N 后,动态获取机器部署情况 M,然后下发单机限流值 N/M。要求请求均匀访问,部署机器统一。
维护全局 key,以时间戳建 key。有热 key 问题,可以通过增加更细粒度的 key 或者定时更新 key 的方法。
对于单用户/单 ip 需要频控,主要是防黑产和恶意用户。如果秒杀是有条件的,例如需要完成 xxx 任务,解锁资格,对于获得资格的步骤,可以进行安全扫描,识别出黑产和恶意用户。
逻辑层
逻辑层首先应该进入校验逻辑,例如参数的合法性,是否有资格,如果失败的用户,快速返回,避免请求洞穿到 db。
异步补单,对于已经扣除秒杀资格的用户,如果发货失败后,通常的两种做法是:
事务回滚,回滚本次行为,提示用户重试。这个代价特别大,而且用户重试和前面的重试策略结合的话,用户体验也不大流畅。
异步重做,记录本次用户的 log,提示用户【稍后查看,正在发货中】,后台在峰值过后,启动异步补单。需要服务支持幂等
对于发货的库存,需要处理热 key。通常的做法是,维护多个 key,每个用户固定去某个查询库存。对于大量人抢红包的场景,可以提前分配。
存储层
对于业务模型而言,对于 db 的要求需要保证几个原则:
可靠性
主备:主备能互相切换,一般要求在同城跨机房
异地容灾:当一地异常,数据能恢复,异地能选主
数据需要持久化到磁盘,或者更冷的设备
一致性
对于秒杀而言,需要严格的一致性,一般要求主备严格的一致。
实践——微视集卡瓜分系统
微视集卡瓜分项目属于微视春节项目之一。用户的体验流程如下:
架构图客户端主要是微视主 app 和 h5 页面,主 app 是入口,h5 页面是集卡活动页面和瓜分页面。
逻辑部分为分:发卡来源、集卡模块、奖品模块,发卡来源主要是任务模块;集卡模块主要由活动模块和集卡模块组成。瓜分部分主要在活动控制层。
奖品模块主要是发钱和其他奖品。
为了做好瓜分时刻的高并发,对整个系统需要保证两个重要的事情:
全链路梳理,包括调用链的合理性和时延设置
降级服务预案分析,提升系统的鲁棒性
如下图所示,是针对瓜分全链路调用分析如下图,需要特别说明的几点:
时延很重要,需要全链路分析。不但可以提高吞吐量,而且可以快速暴露系统的瓶颈。
峰值时刻,补单逻辑需要关闭,避免加剧雪崩。
我们的降级预案大概如下:
一级预案,瓜分时刻前后 5 分钟自动进入:
入口处 1 分钟内陆续放开入口倒计时,未登录用户不弹入口
主会场排队,进入主会场以 100wqps 为例,超过了进入排队,由接入层频控控制
拉取资格接口排队,拉取资格接口 100wqps,超过了进入排队,由接入层频控控制
抢红包排队,抢红包 100wqps,超过了进入排队,由接入层频控控制
红包到账排队,如果资格扣除成功,现金发放失败,进入排队,24 小时内到账。异步补单
入口处调用后端非关键 rpc:ParticipateStatus,手动关闭
异步补单逻辑关闭。
二级预案,后端随机丢请求,接入层频控失效或者下游服务过载,手动开启进入
三级预案,前端随机丢请求,后端服务过载或者宕机进入。手动开启
综上,整个瓜分时刻体验如下所示:
回顾下漏斗模型,总结下整个实践:
高可用秒杀系统设计
说起秒杀,我想大家肯定不陌生,从双十一购物到春节抢红包,再到12306抢火车票,“秒杀”的场景处处可见。简单来说,秒杀就是在同一个时刻有着大量的请求争抢购买同一个产品并完成交易的过程,用技术的话来说就是大量的并发读和并发写。从系统层面来看,秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统,下面我们就来了解一下通过怎样的技术实践来设计一个稳定的高、性能的秒杀系统。
秒杀系统的特点
快:响应快,处理请求速度快,秒杀涉及到大量的并发读和并发写,在并发数比较大的情况下,如何保证系统的TPS和QPS是关键。
准:系统的准确性,针对秒杀系统可以理解为数据的一致性,有限的商品同一时刻被多倍的请求同时来扣减库存,在大并发更新的过程中需要保证数据的准确性,库存为100的秒杀商品,最终只能卖100个,控制资损是关键。
稳:系统的稳定性要求高,在大并发的情况下能保证系统的稳定运行,不出现系统运行故障。
了解了秒杀系统的特点之后,我们就可以有针对性的去设计系统的架构,落实具体的技术方案,具体如下:
一、高性能
动静分离。把用户请求的数据分为"动态数据"和"静态数据",不需要让用户每次都去刷新整个页面,用户只需要点击"一键刷宝"等功能按钮就可以看到最新的秒杀详情。针对静态数据我们可以做静态缓存,静态数据缓存主要涉及以下两个重点:
1. 把静态数据缓存在离用户最近的地方
常用的静态缓存主要有三种,用户浏览器里、CDN上或者后台服务器的Cache里,该根据情况把数据缓存在离用户最近的地方
2. 直接缓存HTTP链接
相对于普通的数据缓存而言,HTTP链接缓存则更加的简单粗暴,直接缓存HTTP链接而不仅仅是数据,如下图所示,Web代理服务器根据请求的UTL直接取出HTTP请求对应的响应头和响应体然后直接返回,这个响应过程简单的连HTTP协议都不需要重新组装,甚至连请求头都不需要解析
动静分离系统部署架构图
热点数据分离:热点数据顾名思义就是访问较为频繁的数据,而热点数据又分为"静态热点数据"和"动态热点数据"。比如秒杀的商品列表、商品详情等,这些我们在秒杀之前都是可以提前知道,而且这部分数据是不会修改的,所以针对这部分数据我们可以直接缓存在服务器Cache里。动态热点数据是在交易过程中产生的,比如一个秒杀商品,由于商户打了一个广告,导致商品点击率骤然上升,那么这种数据就属于动态热点数据。获取动态热点数据则需要一个动态热点发现系统,这里给出一个热点发现系统的简单实现:
1. 构建一个异步系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key如 Nginx、缓存、RPC 服务框架等这些中间件
2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,把上游已经发现的热点透传给下游系统,提前做好保护,比如缓存或者隔离
3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商会被频繁调用,然后做热点区分
热点数据发现系统架构图
业务操作拆分: 将整个交易过程分为多个步骤,用户的一次秒杀抢购涉及到很多后台的流程,比如库存判断、下单、支付和发货等。类似于活动中的抽奖发红包,抽奖和发红包是两个过程,用户一次请求提示中奖,这个时候并没有立刻发放奖品,而是由后台异步程序去发放。同样秒杀也是一样,针对抢购完成之后的一些流程,可能涉及到第三方接口调用,如
果同步去做无疑会使调用链加长,增加响应时间。可以使用MQ消息中间件来完成后续的操作。同时为了保证订单和发货的一致性,可以做定时JOB来保证数据准确性
二、库存扣减实现
针对秒杀系统不超买是前提,如果在高并发扣减库存的情况下保证不超买,同时还要保证用户请求的实时响应速度是关键。一般扣减库存有以下几种方式:
下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是有些人下完单可能并不会付款,这就会导致一些竞争商家恶意下单的情况。
付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了,用户体验不好。
预扣库存,买家下单后,库存为其保留一定的时间(如 30 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买,在买家付款前,系统会校验该订单的库存是否还有
保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付,如果预扣成功,则完成付款并实际地减去库存。
由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后不付款的情况比较少,再加上卖家对秒杀商品有严格的库存限制,所以秒杀商品采用"下单减库存"更为合理,在逻辑处理上更加简单,性能也更占优势。同时可以用后台JOB去检测订单时间和订单状态,根据业务需求可以灵活的回补库存,下单和最终的付款步骤都需要强制校验库存。"下单减库存"在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,库存扣减主要有以下几种方案:
1. 数据库实现扣减:通过数据库事务来实现库存扣减,每次扣减update数据库的剩余库存值,如果更新后的值为负数,就回滚事物,返回无库存。由于扣减是在高并发场景下,所以需要保证每次事物执行的数据准确性,一般有两种SQL语法可选择:
数据库悲观锁:利用数据库for update悲观锁,保证每次只有一个线程在查询数据,在更改数据之前一直锁住该条记录,修改完释放
CASE WHEN判断:在更改的时候判断当前数据是否满足条件,如果满足就就更新为新的值
UPDATE t_stock SET inventory = CASE WHEN inventory>=xxx THEN inventory -xxx ELSE inventory END
基于数据库的实时库存扣减实现起来比较简单,不会有超出的风险。但是这样就会导致所有的读写操作全部
在同一时间集中在数据库上,不仅造成了数据的压力,同时也会影响数据库的执行性能,进而影响请求的响
应时间。
2. 库存+消息实现库存扣减:秒杀商品和普通商品的减库存还是有些差异的,例如商品数量比较少,交易时间段也比较短,因此可以考虑把秒杀商品的库存直接放到缓存系统中实现,也就是直接在缓存中实现库存
的扣减。推荐使用带有持久化的缓存系统Redis,利用Redis单线程的特性,实现数据自增自减的原子性。而消息系统(MQ)则是用来同步缓存和数据库,每次在Redis扣减库存,同时发一条扣减库存的消息,异步
去更新数据库。如果担心缓存的可用性,可以设置库存在缓存中的失效时间,隔一段时间去数据库查询(总数-已抢购的)真实库存同步到缓存中。
如果你的秒杀活动对于库存扣减没有复杂的逻辑,采用缓存管理库存是比较好的方案。但是如果有比较复杂的减库存逻辑,或者需要使用事务,你还是必须在数据库中完成库存扣减
三、高可用
在大量请求和并发数比较大的情况下,我们需要采取一些措施来保证系统不会崩坏,能够稳定的运行,下面列举几种主要的方法:
流量削峰:秒杀场景的一个主要特点就是访问集中,但是最终能抢到商品的人数是固定的,也就是说100个人和1000个人来访问结果是一样的。我们希望更多的人来刷页面,但是下单的请求并非是越多越好,这样的话我们就没必要把所有的请求全都放到对应的服务上,可以设计一些规则来延缓请求,这就是所谓的流量削峰,这里提供两种方案:
1. 消息队列拦截请求。在实际操作中我们可以使用消息队列来缓冲瞬时流量,把同步的调用直接转成异步的推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑的将消息推送出去。当处理到某一个时刻(库存已经不足),后面的请求可以直接返回。
2. 并发限制。我们可以通过增加流程的复杂度来降低并发数,比如秒杀系统,在下单之前我们让用户输入验证码或者答题之类的,这样就可以控制并发提交,保证在最终的下单那一步,并发量没有那么高。这种方式同时也可以防止秒杀器来刷商品,增加了系统的安全性 。
用户限流。在某一时间段内只允许用户提交一次请求,比如可以采取IP或者登录手机号限流。或者针对用户请求的唯一标识uid之类的,在服务端控制层需要针对同一个访问uid,限制访问频率。
数据保证最终的一致性:数据保证最终的一致性就好,没必要保证实时的一致性,这样可以将业务操作拆分出来。比如针对回补库存的操作,扣减库存成功后,如果下单过程最终失败需要回补库存,这个时候只需要保证最终库存回补成功即可(消息异步操作),不需要实时操作,这样就增加了接口的响应速度。
兜底方案:没有人能够提前预估所有情况,意外无法避免,具体到秒杀这一场景下,为了保证系统的高可用,我们必须设计一个Plan B 方案来兜底,这样在最坏情况发生时我们仍然能够从容应对。
1)开关系统。当系统容量达到一定程度时,这个时候就需要对请求进行降级、限流或者直接拒绝服务,而这些操作都需要在项目中设置好控制开关,比如什么时候流量限制设置多大,开关的触发都是有一定规则的。它分为两部分,一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表;另一部分是执行下发开关数据的 Agent,主要任务就是保证开关被正确执行,即使
系统重启后也会生效。来关闭活动,直接提示库存不足。
2)备用接口。一旦请求失败,进入备用数据接口请求备份数据。这里提到的备用接口,主要是数据的硬兜底,也就是说针对非个性化的请求,用户多次访问的结果一样。在系统瘫痪的情况下,接口去请求兜
底数据。至于兜底数据的来源主要有两个:
- 后端向 CDN push 一份通用数据。我们知道个性化都是使用 cookie 去识别用户的,对于没有浏览器记录的新用户就没有 cookie,此时会推一份通用的数据,这个通用的数据也可以作为接口的备份源
四、架构设计
系统架构
五、总结
网站的高可用建设是基础,可以说深入到各个环节,本章所讲的秒杀系统是一个比较典型的用例,只有了解系统的特点、业务上的要求,我们才可以有针对性的去设计系统的架构,实施具体的技术方案。同时在系统的设计上面我们要长期规划并进行系统化的建设,需要在预防、管控、监控和恢复系统等这些地方加强建设,如果要保证系统的完整性,每一个环节都有很多事情需要做。
更多福利请关注官方订阅号“拍码场”
好内容不要独享!快告诉小伙伴们吧!
喜欢请点击↓↓↓
以上是关于如何设计一个高可用高并发秒杀系统的主要内容,如果未能解决你的问题,请参考以下文章