关于微信红包的架构思考
微信红包的架构实现是前段时间技术圈里很热门的一个话题,这是一个非常典型的大访问高并发场景。至于如何实现,答案并不是唯一的,对这个问题的思考其实反映了工程师的架构设计功底。所以我也谈一谈我的想法。
首先任何架构设计离不开对业务的理解和认识,架构设计并不是凭空存在的,一定是基于业务场景并且服务业务场景的。所以我先分析一下微信红包的业务场景。
*** 需要特别说一点,本文中的架构设计不一定是官方的实现方式,只是一种分析,是基于我目前的技术能力的一种思考 ***
业务场景分析
根据微信红包的操作,可以把红包的业务场景分为发放红包,抢红包和打开红包三个步骤。
PS:我觉得微信红包的架构讨论之所以非常火爆,一个重要原因就是微信红包的业务场景大家都很熟悉
发放红包:
- 发放者设置红包金额,红包数量等属性
- 从发放者账户扣除红包金额
- 生成红包,并且发送抢红包的链接
抢红包:
- 用户点击红包链接
打开红包:
- 显示用户抢到的红包金额
- 剩余红包数量-1,红包剩余金额改变
- 抢到红包的用户账户金额增加
注意,以上均为原子操作
根据业务场景,可以知道红包的主要属性有:
- 红包金额:始终不变
- 红包总数量:始终不变
- 红包剩余金额:每次有人领取红包,该属性变化
- 红包剩余数量:每次有人领取红包,该属性-1
- 抢到红包的用户以及抢到的金额:用于显示手气排行,并且防止重复抢红包
- 过期时间:如果到过期红包仍未抢完,返回发放者账户
架构分析
单纯实现微信红包的功能并不复杂,其难点在于如何处理高访问和并发,当发出一个红包之后,只有少数人能够抢到,而大部分的请求都属于无效请求,如果让这部分请求落到数据库或者在服务端经过复杂处理,那么以微信的用户体量,后果是灾难性的。
从这个角度来说,微信红包和电商的秒杀业务有几分相似
处理类似业务最常见的手段就是请求过滤和添加缓存(cache),当然,至于服务器集群,负载均衡,读写分离这些基础架构,由于已经成了大型网站的标配,默认已经存在。
其中,请求过滤是只允许少数符合条件的请求走到最后,把大量不符合条件的请求挡在外面,这个非常关键。而采用缓存技术,原因有两个,一是缓存的访问速度快,使用缓存可以有效提高吞吐速度,二是红包发放相对来说是一个临时性的东西,故而可以放在缓存里面。
业务上需要注意的地方
- 防止用户重复抢红包
- 应对redis宕机的风险,目前一般是采用sentinel机制
架构设计
缓存我们默认使用redis,数据库默认使用mysql
新建红包的时候,生成唯一红包id,设置红包属性,在mysql中添加记录,表结构大致如下
|红包id|发放者uid|红包总金额|红包总数量|红包剩余金额|红包剩余数量|抢到红包的用户列表json|过期时间|创建时间|
|---|:----|:---|---|----|---|---|----|---|--|--|:---|:----|:---|---|----|---|---|----|---|--
同时,在redis里添加:
- 抢红包请求队列(队列)
- 打开红包请求队列(队列)
- 红包信息,包含剩余红包数量和剩余金额(键值对)
- 已经抢到红包的用户信息(有序集合)
当抢红包的时候,
做以下判断:
- 剩余红包数量是否大于0
- 该用户是否在已经抢到红包的用户集合里面
通过判断的请求添加进请求队列,否则返回已经抢完的标示,这样下一步用户打开红包的时候,甚至可以不发送请求直接显示红包已经抢完,过滤无效请求
于此同时,另外有进程从请求队列里取出抢红包请求,生成token标示,返回给抢红包者
当用户打开红包的时候,传入该标示,放进打开红包请求队列,后端消费者进程从队列中取出打开红包请求,判断红包数量是否大于0,以及用户传来的打开红包token与红包id是否合法,参数检验之后,生成红包金额,调用余额接口处理,并且更新redis中红包剩余数量,红包剩余金额
最后异步方式更新数据库
红包算法
关于红包算法,网上说的很多了,这里就不啰嗦了,金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储,具体的算法是在在0.01和剩余平均值的2倍之间随机生成一个金额
效果
比如现在发了一个红包,分为5份。有100个人抢。
可能有10个人成功进入请求队列(抢到红包和打开红包之间有一定间隔,所以先抢到的不一定先打开),拿到了打开红包token,其余90个人拿到的是红包已经抢完的token,那么当打开红包的时候,这90个人甚至都不需要发送请求,直接显示红包已抢完
这10个人打开红包的时候,其中5个人抢到,剩下的5个也显示红包已经抢完
本来5个红包100个人抢,会请求201次(生成红包1次+抢红包100次+打开红包100次)
那么这种情况下,只需要111次(生成红包1次+抢红包100次+打开红包10次),并且,其中90次请求很快就返回,大大减轻服务端压力
需要注意的是
- 如何保证原子性操作
- 如何提高可用性,主要是防止redis宕机
总结
衡量一个架构设计的标准主要是性能,扩展性,伸缩性,可用性,安全性等指标,本例中,由于红包业务是一个很具体的业务,并非通用服务,所以不讨论扩展性。
- 性能角度,由于采用了缓存以及请求过滤等手段,性能肯定比常规实现大大提高
- 伸缩性角度,应该采用服务器集群,redis集群,mysql集群的方式部署,注意redis集群中的一致性hash的实现
- 可用性角度,本例中决定可用性的关键还是在于redis集群是否能够保证高可用,所以sentinel机制必不可少,甚至还应该加上其他的各种检测节点,替换节点方案,以保证高可用。而且需要考虑极端情况,例如缓存被击穿导致mysql压力过大的情况下,如何确保服务不宕机。
- 安全性角度,红包涉及资金来往,所以需要格外注意安全性。红包业务中,是通过调用接口来实现资金往来,故账目平衡的风险不是由红包系统来承担而是由红包所调用的接口保证的,但红包系统也可能存在以下几种安全隐患,单一用户重复抢红包,所有用户抢的的红包金额总数大于发放者设置的红包金额等。所以其关键在于确保抢红包,打开红包均为一次原子性操作。如有必要,甚至可以通过加锁等方式实现,但需要考虑加锁对于性能上的影响