借鉴秒杀思想设计直播抽奖,将50QPS提升至4W

一、前言

先谈谈秒杀的设计思路

秒杀业务设计大致分为三块:前端网关后端

前端需要动静分离,可以采用第三方文件服务器获取静态资源,减轻CDN带宽压力,提升访问秒杀页面的用户体验。秒杀业务大致都是异步完成,所以需要前端轮训秒杀的请求结果。

网关需要对请求进行限流(分布式锁)、保护或者IP黑白名单控制,防止单用户短时间多次请求。

后端采用数据库的sql where 库存>0来防止库存超卖,为了减少数据库访问压力,也需要在redis中生成对应库存数量的令牌桶供请求获取(或者在redis记录库存进行扣减),获取到令牌的请求可以向MQ投递消息,从而流量削峰。MQ的listener做DB的写入,前端的轮训请求查询DB

二、背景

接到需求,需要配合直播服务在晚间直播时,提供抽奖能力,硬性指标是2WQPS。先简单压测了抽奖系统的抽奖接口,50多的QPS不忍直视。所以开始阅读抽奖接口的代码,开始漫长的调优一路。。。

脱离业务谈技术,容易挨骂,所以先简单描述下业务场景,我负责奖励系统和抽奖系统。奖励系统提供发放奖励能力,抽奖系统提供抽奖能力。用户需要先获取抽奖机会,才能进行抽奖。而抽奖机会是通过奖励系统来发放的(一般都是用户完成某个任务,触发任务绑定的奖励,来发放奖励,抽奖机会只是奖励的一种类型,奖励类型还有积分,优惠券等等)。

按照产品设计,用户来直播间抽奖,需要调用奖励系统的发放奖励接口发放抽奖机会(一人一次),然后当直播间发起抽奖时,用户点击抽奖按钮,会调抽奖系统的抽奖接口,抽奖接口会判断用户是否有抽奖机会,然后根据抽奖算法算出奖品,扣减库存,发放奖品。

三、实战

这里有个隐患:奖励系统发放奖励的接口是交由MQ异步完成的,高并发的情况下,消息会出现堆积。如果消息没有消费,用户就来抽奖,会出现抽奖机会不足的提醒,有客诉风险。

1、初始版本

秉着一人只能抽一次的产品设计,我们开始思考:废那么大劲,去为一个用户发放一次抽奖机会,在抽奖接口中消耗掉,这一增一减的操作是否可以舍弃?于是,参照原抽奖接口的逻辑,单独开发了一个直播抽奖接口。

1.访问抽奖接口时,先来一通业务校验,看看用户是否满足抽奖的条件。

2.记录抽奖机会总发放次数(数据库update同一行数据,服务于B端运营同事的看板)。

3.执行抽奖逻辑,选出中奖的奖品。

4.扣减库存(数据库乐观锁,即update 库存表 set 已发放库存 = 已发放库存 +1 where 库存 > 已发放库存)。

5.DB写入中奖记录,并调奖励系统发放奖品。

6.返给调用方抽中的奖品。

为了减少mysql的压力,很多查库的操作都做了Redis缓存,并在调用直播抽奖接口前,会先执行预热缓存的动作,来防止热点数据击穿。3台机器100并发的情况下qps只有56,接口平均响应时长1773ms,很明显,代码需要继续优化。

2、借鉴秒杀

在梳理方案时,感觉需求类似秒杀,都是在短时间内抢资源。参照秒杀方案,我把写库的操作都放在了MQ来异步完成。

1.redis分布式锁幂等校验。

2.redis的zset进行时间窗口限流(可配置)。

3.redis的原子自增incr用户抽奖次数校验(一人一次)。

4.业务校验。

5.抽奖逻辑,选中奖品。

6.redis扣减库存,扣减成功的请求向RabbitMQ投递消息,扣减失败的请求返回未中奖。

7.RabbitMQ的listener记录抽奖机会总发放次数,乐观锁扣减库存,写入中奖记录,发放奖品。

3台机器:

500并发,最大qps 5100,平均RT 88ms,90%RT 200ms。

1000并发,最大tps 5000,平均RT 200ms,90%RT 632 ms。

查看服务资源使用峰值 70%,load持续增加,达到性能瓶颈;

排查消费队列,队列累计堆积160w条消息,处理速度约 140/S。

数据库存在行级锁的慢sql。

即qps不满足,服务器资源占用高,队列消息堆积,数据库慢sql堆积。

3、优化慢SQL和慢消费

再修改数据库的抽奖机会发放次数和奖品已发放库存,本质上都是对数据库中表的同一行数据做update。

update 库存表 set 已发放库存 = 已发放库存 +1 where 奖品id = xxx and 库存 > 已发放库存;

update 抽奖机会表 set 已发放数 = 已发放数 +1 where 抽奖机会id = xxx;

思考了一种方案,把这种update操作攒起来,放入Redis中,通过定时任务来同步Redis和DB,减轻DB的压力。

4、拆分队列

产品提出新的需求,抽奖结束后5min内提供中奖名单,也就是5min内消费完这些消息。以2wQPS预估,实际中奖者占比较小,那么队列中存在的消息大多数都是未中奖的消息。故而,我们需要根据抽奖结果,拆分队列为:中奖队列和未中奖队列。剔除未中奖消息的干扰,提升中奖队列的消费能力。

5、接入SkyWalking和sentinel

方便定位链路耗时,接入了SkyWalking监控;组内有自己搭建的sentinel,所以限流从服务代码,移到了sentinel中。

6、生产集群和消费集群的拆分

为了提升高可用,降低CPU使用率。我们觉得让服务器各司其职,拆分服务机器为生产集群和消费集群。但同代码同配置如何做到拆分?在启动脚本中设置一个参数,用于区分生产或者消费角色,然后程序的listener在注入spring容器时,使用spring的@Condition判断,如果启动参数配置是生产者,则放弃注入(返回false)。在nginx的upstream中去掉消费集群的列表,停止流量接入,这样就完成了生产集群和消费集群的拆分。

25台生产者5台消费者

抽奖平台接口 最大qps 13000,持续不到1分钟 迅速下降。

redis-qps峰值 36w。

抽奖平台 1分钟左右tps 会下降到 10000以下,排查发现,入队操作和redis操作存在大于1秒的现象。

通过skywalking的追踪,发现有些Redis的操作会比较耗时,MQ的投递也会耗时,性能衰减严重。

7、优化Redis和MQ

为了寻找Redis的瓶颈,统计了接口涉及Redis的操作,竟然高达20~25次,惨!针对于请求重复key的操作进行抽取,再对可原子操作的请求使用pipeline进行批处理,但也仅仅减少了3处。组内大哥的建议是引入localCache,为了保证产品的设计不变,打死不从(多级缓存的数据同步是个难题)。后来老师建议使用谷歌的localCache,可以设置有效期,有效期内不允许修改即可,我开始动摇了(真香~)。按照这种方式说服产品后,开始调整,又将Redis的操作减少了4次。剩余的就是Redis的热key问题了,原计划将热key放入另一台Redis中隔离,由于性能达标,故最终未采用。

8、最终结果

25台生产+5台消费+mq5wQPS+1w并发时,QPS峰值4w,平均值3w,平均响应时长148ms,90%响应时长320ms,MQ峰值3.5w,Redis峰值29w。

四、心得

纸上得来终觉浅,绝知此事要躬行!以前对于学来的知识总是觉得自己可以信手拈来,实战时才发现未知的风险和遗漏比比皆是。对于未涉足过的领域还是要心存敬畏,真正的大师永远怀着一颗学徒的心!

欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!

公众号:帝都的雁

 

 

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值