如何设计一个百万级用户的抽奖系统?

    xiaoxiao2021-04-15  222

    1、抽奖系统的背景引入

    本文给大家分享一个之前经历过的抽奖系统的流量削峰架构的设计方案。

    抽奖、抢红包、秒杀,这类系统其实都有一些共同的特点,那就是在某个时间点会瞬间涌入大量的人来点击系统,给系统造成瞬间高于平时百倍、千倍甚至几十万倍的流量压力。

    比如抽奖,有一种场景:某个网站或者APP规定好了在某个时间点,所有人都可以参与抽奖,那么可能百万级的用户会蹲守在那个时间点,到时间大家一起参与这个抽奖。

    抢红包,可能是某个电视节目上,突然说扫码可以抢红包,那么电视机前可能千万级的用户会瞬间一起打开手机扫码抢红包。

    秒杀更是如此,所谓秒杀,意思是让大家都在电脑前等着,在某个时间突然就可以抢购某个限量的商品

    比如某个手机平时卖5999,现在限量100台价格才2999,50%的折扣,可能百万级的用户就会蹲守在电脑前在比如凌晨12点一起点击按钮抢购这款手机。

    类似的场景其实现在是很多的,那么本文就用一个抽奖系统举例,说说应对这种瞬时超高并发的流量,应该如何设计流量削峰的架构来应对,才能保证系统不会突然跨掉?

     

    2、结合具体业务需求分析抽奖系统

    假设现在有一个抽奖的业务场景,用户在某个时间可以参与抽奖,比如一共有1万个奖,奖品就是某个礼物。

    然后参与抽奖的用户可能有几十万,一瞬间可能几十万请求涌入过来,接着瞬间其中1万人中奖了,剩余的人都是没中奖的。然后中奖的1万人的请求会联动调用礼品服务,完成这1万中奖人的礼品发放。

    简单来说,需求场景就是如此,然而这里就有很多的地方值得优化了。

     

    3、一个未经过优化的系统架构

    先来看一个未经过任何优化的系统架构,简单来说就是有一个负载均衡的设备会把瞬间涌入的超高并发的流量转发到后台的抽奖服务上。

    这个抽奖服务就是用普通的Tomcat来部署的,里面实现了具体的抽奖逻辑,假设刚开始最常规的抽奖逻辑是基于MySQL来实现的,接着就是基于Tomcat部署的礼品服务,抽奖服务如果发现中奖了需要调用礼品服务去发放礼品。

    如下图所示:

     

    4、负载均衡层的限流

    4.1 防止用户重复抽奖

    首先第一次在负载均衡层可以做的事情,就是防止重复抽奖。

    我们可以在负载均衡设备中做一些配置,判断如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖,或者是他们自己写的脚本在刷奖,这种流量一律认为是无效流量,在负载均衡设备那个层次就给直接屏蔽掉。

    举个例子,比如有几十万用户瞬间同时抽奖,最多其实也就几十万请求而已,但是如果有人重复抽奖或者是写脚本刷奖,那可能瞬间涌入的是几百万的请求,就不是几十万的请求了,所以这里就可以把无效流量给拦截掉。

    如下图所示:

    4.2 全部开奖后暴力拦截流量

    其实秒杀、抢红包、抽奖,这类系统有一个共同的特点,那就是假设有50万请求涌入进来,可能前5万请求就直接把事儿干完了,甚至是前500请求就把事儿干完了,后续的几十万流量是无效的,不需要让他们进入后台系统执行业务逻辑了。

    什么意思呢?

    举个例子,秒杀商品,假设有50万人抢一个特价手机,人家就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求没必要让他转发到Tomcat服务中去执行秒杀业务逻辑了,不是吗?

    抽奖、红包都是一样的 ,可能50万请求涌入,但是前1万个请求就把奖品都抽完了,或者把红包都抢完了,后续的流量其实已经不需要放到Tomcat抽奖服务上去了,直接暴力拦截返回抽奖结束就可以了。

    这样的话,其实在负载均衡这一层(可以考虑用Nginx之类的来实现)就可以拦截掉99%的无效流量。

    所以必须让抽奖服务跟负载均衡之间有一个状态共享的机制。

    就是说抽奖服务一旦全部开奖完毕,直接更新一个共享状态。然后负载均衡感知到了之后,后续请求全部拦截掉返回一个抽奖结束的标识就可以了。

    这么做可能就会做到50万人一起请求,结果就可能2万请求到了后台的Tomcat抽奖服务中,48万请求直接拦截掉了。

    我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。

    当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。

    下图展示了上述所说的过程:

     

    5、Tomcat线程数量的优化

    其次就是对于线上生产环境的Tomcat,有一个至关重要的参数是需要根据自己的情况调节好的,那就是他的工作线程数量。

    众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。

    但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。

    但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通虚拟机的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。

    同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。

    所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。

    在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。

    但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。

     

    6、基于Redis实现抽奖业务逻辑

    现在问题又来了,虽然在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务

    此时抽奖服务自然是可以多机器来部署的,比如假设一台Tomcat可以抗500请求,那么2万并发就是40台机器。

    如果你是基于云平台来部署系统的,搞活动临时租用一批机器就可以了,活动结束了机器立马可以释放掉,现在云平台都很方便。

    但是有个问题,你的数据库MySQL能抗住2万的并发请求吗?

    如果你基于MySQL来实现核心的抽奖业务逻辑,40个Tomcat部署的抽奖服务频繁对MySQL进行增删改查,这一个MySQL实例也是很难抗住的。

    所以此时还得把MySQL给替换成Redis,通常这种场景下,建议是基于Redis来实现核心的业务逻辑。

    Redis单机抗2万并发那是很轻松的一件事情,所以在这里又需要做进一步的优化。如下图:

     

    7、发放礼品环节进行限流削峰

    接着问题又来了,假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。

    礼品服务假设也是优化后的Tomcat,可以抗500并发,难道礼品服务也要去部署20台机器吗?

    其实这是没必要的,因为抽奖之后完全可以让礼品服务在后台慢慢的把中奖的礼品给发放出去,不需要一下子就立马对1万个请求完成礼品的发放逻辑。

    所以这里可以在抽奖服务和礼品服务之间,引入消息中间件,进行限流削峰。

    也就是说,抽奖服务把中奖信息发送到MQ,然后礼品服务假设就部署两个Tomcat,慢慢的从MQ中消费中奖消息,然后慢慢完成1完礼品的发放就可以了。

    假设两个礼品服务实例每秒可以完成100个礼品的发放,那么1万个礼品也就是延迟100秒发放完毕罢了。

    也就是你抽奖之后,可能过了一两分钟,会看到自己的礼品发放的一些物流配送的进度之类的。

    而且礼品服务可能需要在MySQL数据库中做很多增删改查的操作,比如插入中奖纪录,然后进行礼品发货等等。

    此时因为礼品服务就2个Tomcat实例,所以对MySQL的并发读写不会太高,那么数据库层面也是可以抗住的。

    整个过程,如下图所示:

     

    8、系统架构设计总结

    其实对于商品秒杀、抽奖活动、抢红包类的系统而言,架构设计的思路很多都是类似的,核心思路都是对于这种瞬时超高流量的系统,尽可能在负载均衡层就把99%的无效流量拦截掉

    然后在1%的流量进入核心业务服务后,此时每秒并发还是可能会上万,那么可以基于Redis实现核心业务逻辑 ,抗住上万并发。

    最后对于类似秒杀商品发货、抽奖商品发货、红包资金转账之类的非常耗时的操作,完全可以基于MQ来限流削峰,后台有一个服务慢慢执行即可。

    我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

    作者:狸猫技术窝 链接:https://juejin.im/post/5ce1975af265da1bd42450b5


    最新回复(0)