微信抢红包设计

A

现在的工作或面试中,很多人动辄就会被问到高并发的问题,但并不是所有研发同学都有机会参与到并发很高的项目中,很多人准备这块内容也纯粹是为了面试,真正的工作中,还是单纯的CRUD,那么高并发就不重要了吗?那是相当的重要,重要到你不会就通不过面试的程度。高并发可以是高接口、高服务、高系统,这次就从微信抢红包来简单的说说这块。

在设计之前,先来拆解一下微信抢红包的大致流程:发红包、扣款、收红包、拆红包、领取金额、入账、查看红包领取记录、红包被领取通知。从这7个步骤着手简单分析一下(实际不止这几个),这几项哪些是会影响用户体验的呢?感觉每一个都会,但相对来说收红包、拆红包的实时性体验更重要,并且这两者属于是并发量最高的,比如在一个100人的群中发一个10人份的红包,来分析一下请求量:

Action Request Quantity
发红包 1
扣款 1
收红包 100
拆红包 100
领取金额 10~100
入账 10
查看红包领取记录 100+
红包被领取通知 10
其他

实际上对于一个红包来说,可能带来群内用户数的请求量,如果放在一个五百人的群中,那么一个收红包的请求数可能就会达到500,春晚期间每秒百万个红包,则带来的请求量就是一秒几个亿的级别,当然这是一个理论上的数据,如果想要达到几个亿的并发量,那我不会~

分析

1. 硬件

硬件么,也就是服务器了,常常被调侃的往往最有效:出现了并发瓶颈怎么办?答:加机器!加内存!加磁盘!加带宽!是不是当听到这个回答之后感觉对方挺没意思,那么如果在工程的JVM、代码、队列、缓存等等一系列都完善好了,这时候怎么办?只能具体问题具体分析了:如果瓶颈在内存上,那么就加内存;如果瓶颈在磁盘上,空间不足那么就加磁盘,写读速度太低,那么就换SSD;如果瓶颈在CPU上,那么就加机器,横向扩充CPU;如果是网络问题,那么就加带宽。

通过以上分析,是不是觉得加机器还是挺有道理的。加机器会带来的优势:内存大、磁盘读写快、CPU管够、网络嗖嗖的

2. 代码

在加机器之前,都应该先把服务工程的代码优化到极致,代码优化就不细说了,属于是日常积累。

3. 锁

乐观锁和悲观锁的正确使用能够带来不同的性能指标,

设计

1. 发收红包

红包的并发有多高?简单的计算一下:十万个人,每个人同时发一个红包,发红包的并发量就是十万,但实际上在高峰期时期,不可能只有十万个人在发红包。拒资料记载:在2017年除夕当天,收发微信红包的数量达到了142亿,按照一天86400秒计算的话,平均每秒就有16万个红包发出,也就是持续一整天每秒并发16万,当然流量不可能这么平均,资料上记录当天最高峰值达到了76万每秒,是平均值的5倍左右。

发红包是允许发送失败(告诉用户网络问题,让用户自我怀疑),但不允许发送成功之后丢失数据,因为涉及到钱,数据安全性和一致性的要求必须非常高,先不考虑数据的安全性和一致性,这里可以使用中间件来保证。在并发量较高的情况下,发红包请求发出之后,服务器并不能每一个请求都及时处理,所以这里如果把同步搞为异步,会解决什么问题(不涉及扣款):

同步

接收发红包请求->通知群内所有人->全部通知完成->反馈用户红包发送成功->准备接收发红包请求

从同步请求来看,一台实例每次只能处理一个请求,如果红包通知的目标人群数量庞大,那么这将是非常耗时的操作,并且如果其中一个用户通知失败(中途有人退群),还可能会影响到全部的通知,造成的延迟将不可预估。当每台实例每秒只能处理一个请求的时候,高峰期的76万个并发则需要76万台实例,公司没那么多银子,砸不起。

异步

1. 接收发红包请求->存入消息队列->反馈用户红包发送成功->准备接收发红包请求

2. 发红包消息处理器->拆解消息通知->存入消息队列

3. 通知消息处理器->发送通知

接着就是异步处理,服务端接收到发红包请求之后立即将请求放入发红包消息处理队列,然后立即反馈用户发送成功,这里耗时的地方只有与消息队列通信这一个,在网络正常的情况下,耗时微乎其微。发红包的请求入队之后按顺序排排好,等待消费端进行消费处理(这里要保证消息不能丢失),接下来就是消息处理器的工作了,对于发红包的用户来说红包已经发成功了,其他人啥时候收到就不关我事了,反正我发出去了。

至于消息消费的效率问题,就属于是消息中间件的吞吐量问题了,选择和配置相关参数提升吞吐量不算太难的事情,关键要有银子,不然买不起好的机器也没太大用处。发红包消息处理器对收到的消息进行拆解,按用户进行拆分,将拆分后的数据封装成一条条通知消息,并将通知消息放入到通知消息队列中,至于顺序,那就关系不大了,这里一般不需要保证有序,因为没有必要保证收到消息的用户要按照入群的顺序来啊,不然如果真因为网络问题或者中间件吞吐量问题造成了延迟,本群抢到红包的就永远都是最先入群的那几个人了。

通知消息处理器从队列中依次处理消息,按个儿通知用户,此刻用户就能发现有一个红包出现。消息通知晚的那就只能手慢无了。

所以有时候抢不到红包不一定是你手速慢,也可能是没那个命

2. 拆红包

当我们看到红包之后,也不知道它有没有被抢完,第一反应肯定都是我要点它,点击红包会有一个加载的过程,加载的过程就是在向后台请求当前时刻红包是否已被抢完,如果没被抢完则会出现一个大大的,点击开抢(一直没搞明白为什么不依据定向红包、手气红包这种红包类型变更按钮,比如定向叫拆[没福气被拆迁,可以被拆红包也不错],或者手气红包叫抢[不知道是不是有限制不能用抢这个字])。

image-20210525115828021 image-20210525120415369

那么加载过程是异步还是同步的呢?如果是异步请求,那么请求发送出去立即返回,等异步处理完后再反馈到客户端,如果异步排队的话,那这个请求时长就不可控了,所以这里按照操作行为来看,采用同步请求更适合,既然是同步请求,那么在请求返回之前就需要hang住线程资源,所以就只能想办法把hang住的时间变短,这个时候缓存好像就起到了很大的作用,如果单纯的使用数据库,在数据库资源竞争上又是一个大问题,涉及到数据的变更,必定要和数据库锁打交道,锁又是性能首害,而用Redis的话,天生的单线程处理+reactor主从线程池模型提供了高性能保障,将读写压力从数据库转移到每秒几万并发的Redis,请求时间缩短很多,直接的提升了加载接口的并发量。

那么是不是每一次点击红包都要去后台请求一次状态呢?当然不行,已经被领取完的红包还去后台请求干啥?钱会退回来吗,还是状态会变?所以已经被领取完的红包信息完全可以在客户端缓存一份,每次请求就不用和服务端进行通信了,把流量控制在客户端,减缓服务端的压力,同时也提升了用户体验(没有网络请求比本地请求快的道理),哦吼,好像不错~

点击红包的动作就这,就是一个同步请求+服务端远程缓存+客户端缓存

继续深入思考一下,这几个步骤还有哪一步可以继续优化一下,同步改异步在前面已经说过了,好像可行性不高,客户端缓存可以减少网络请求,性能提升也挺高,好像也没有更优的方案了,那么就只剩服务端远程缓存了,上面讨论到服务端缓存使用Redis,也就是一个分布式缓存,使用Redis的目的就是不论请求路由到哪一个节点都可以访问到同一个资源,大致的流程是这样的:

服务端接收请求->与Redis服务器发送请求->读取Redis数据->Redis服务器返回数据->服务端返回数据

如果采用本地缓存的话,会是什么样子呢?看下流程:

服务端接收请求->读取本地缓存数据->服务端返回数据

少了与Redis服务器建立连接的过程,在网络资源紧张的情况下,每秒几十万次读,好像问题也不大(不要说Redis单机只几万,哪个公司产线会只用一台单机版的Redis),每次读耗时0.00001秒的话,70万次就是7秒,貌似挺多的,并且一次读取也不可能只0.00001秒的时间,所以这里对于当前业务来说属于一个可优化的性能瓶颈,优化的点就是把访问远程内存改为访问本地内存,减少网络开销,直接读取服务实例所在机器内存,效率绝对杠杠的。

使用本地缓存可以提升性能,但是会带来另一个问题:只有关于这个红包的所有请求都发送到固定的一台机器上,才能正确的使用本地缓存,否则如果出现请求离散的情况,本地缓存就是一个诟病了。所以如果想使用本地缓存的话,我们就要控制请求路由,让同一个红包的请求都发送到固定的一台实例上,这要怎么整?我们知道常用的请求路由方式有:轮询、权重、ip-hash、fair(第三方)、url-hash(第三方),轮询好像不行,每一次请求都被路由到不同的实例上,权重好像也不行,ip-hash只能够保证同一个客户端的请求路由到固定的实例,fair是按后端服务器的响应时间来分配请求,更不合适了,还有一种根据url计算hash,也不合适,不可能所有的请求都是一个hash值。常用的路由好像都不合适,但是我们发现了一种能让指定请求都路由到一台实例上的方式,ip-hash和url-hash,那么我们能不能针对红包搞一个read-packet-hash算法,将同一个红包的所有操作请求都路由到一台实例上,貌似可以,那么就不介绍具体实现了,网关层做控制很简单:对每一个红包在创建的时候都生成一个全局唯一ID,后续针对这个ID计算哈希进行路由。

使用本地缓存的话,如果有人杠起来:一台实例挂了的话岂不是这台实例的所有红包数据都丢失了?那你咋说?当然怼回去:Redis某ms节点挂了的话影响的服务实例更多,咋整?只能尽可能的保证节点的稳定性,但也不是说使用了本地缓存就弃用远程缓存,两者是相辅相成、互相同步的关系:本地->远程 && 远程->本地。

3. 领取金额

红包都被拆开了,是不是就可以分钱了呢?是的,可以分钱了,分钱的并发量在红包数量~群内人数之间,为什么说能够达到群内人数这么高的并发,如果所有人都在同一时刻点开红包然后同一秒内点,那么领红包的接口并发量就上来了,领取功能简单,关键就在于金额怎么分配,我们依据单例的懒汉模式和饿汉模式来分析一下:

懒汉模式

懒汉模式就是每一个请求来到之后再做相应的处理,数据处理为同步的。

饿汉模式

饿汉模式就是在请求到来之前,先把数据准备好,数据处理为异步的。


这两种模式应该怎么选择?同步模式下,每次请求都需要通过算法计算一次所得金额,并发情况下需要争抢同一个资源,每一次计算的耗时造成过多的锁等待;而异步模式下,在抢红包请求到来之前,每个人所得金额都已经排好排等着被领走了,只需要依据先来先得的规则按需取走即可,计算时间成本下降了,锁等待时间就相应的下降了,并发就提升了。

1…异步刷库到账

4. 查看红包领取记录

查看红包领取记录属于是一个常规Action,不论什么时候都可以查看过往红包的领取记录,上面讲了可以将已领完的红包数据缓存到客户端,那么在服务端怎么处理?Elasticsearch、MongoDB、HBASE这些先不讨论,就拿MySQL来说,除夕一天142亿个红包,如果平均每个红包都有10个小包,那就是1420亿条领取记录,突然一下给整不会了,没处理过这么大的数据量,我先想想