方案一也是最常见的思路,在用户领取时对数据库的红包进行加锁,然后扣减金额,然后释放锁完成整个红包领取。这个方案的优点是清晰明了,但是这种方案的问题会导致多个用户同时来领取红包时,会造成数据库行锁的冲突,需要排队等待,当排队请求过多时会造成数据库链接的浪费,影响整体系统的性能。同时在上游长时间未收到反馈导致超时,用户侧可能会不停重试,导致整体数据库链接被耗尽,从而导致系统崩溃。
红包预拆分方案方案一的问题是多个用同时领取会造成锁冲突,解锁锁冲突可以通过拆分的方式,来将锁化成更细的粒度,从而提高单个红包的领取并发量。具体方案如下:
在方案二中,对发红包的流程进行了一个改动,在发红包时会对红包进行一个预拆分的处理,将红包拆成多个红包,这样就完成了锁粒度的细化,在用户领取红包时从之前的争抢单个红包锁变为现在多个红包锁分配。从而在领取红包时问题就变为如何给用户分配红包,一种常用的思路是当用户请求领取红包时,通过 redis 的自增方法来生成序列号,该序列号即对应该领取那一个红包。但是这种方式强依赖 redis,在 redis 网络抖动或者 redis 服务异常时,需要降级到去查询 DB 还未领取的红包来获取序列号,整体实现比较复杂。
最终方案在视频红包的场景中,整个业务流程是用户拍摄视频发红包,然后在视频推荐 feed 流中刷到视频时,才会触发领取。相对于微信和飞书这种群聊场景,视频红包中同一个红包的领取并发数并不会很高,因为用户刷视频的操作以及 feed 流本身就完成了流量的打散,所以对于视频红包来说,领取的并发数并不会很高。从业务的角度来看,在需求实现上,我们在用户领取完成后需要能获取到未领取红包的个数信息下发给用户展示,方案一获取红包库存很方便,而方案二获取库存比较麻烦。另外从系统开发复杂度和容灾情况看,方案一相对来说是一个更合适的选择。但是方案一中的风险我们需要处理下。我们需要有其他的方式来保护 DB 资源,尽量减少锁的冲突。具体方案如下:
- 红包 redis 限流
- 为尽可能少的减少 DB 锁冲突,首先会按照红包单号进行限流,每次允许剩余红包个数*1.5 的请求量通过。被限流返回特殊错误码,前端最多轮训 10 次,在请求量过多的情况下通过这种方式来慢慢处理
- 内存排队
- 除了 redis 限流外,为了减少 DB 锁,我们在领取流程中加个一个红包内存锁,对于单个红包,只有获取到内存锁的请求才能继续去请求 DB,从而将 DB 锁的冲突迁移到内存中提前处理,而内存资源相对于 DB 资源来说是非常廉价的,在请求量过大时,我们可以水平扩容。
- 为了实现内存锁,我们进行了几个改动。首先需要保证同一个红包请求能打到同一个 tce 实例上,这里我们对网关层路由进行了调整,在网关层调用下游服务时,会按照红包单号进行路由策略,保证同一单号的请求打到同一个实例上。另外我们在红包系统的 core 服务中基于 channel 实现了一套内存锁,在领取完成后会释放该红包对应的内存锁。另外为了防止锁的内存占用过大或者未及时释放,我们起了一个定时任务去定期地处理。
- 转账异步化
- 从接口耗时来看,转账是一个耗时较长的操作,本身涉及和第三方支付机构交互,会有跨机房请求,响应延时较长,将转账异步化可以降低领取红包接口的时延,提高服务性能和用户体验
- 另外从用户感知来看,用户更关注的是领取红包的点击开后是否领取成功,至于余额是否同步到账用户其实感知没那么强烈,另外转账本身也是有一个转账中到转账成功的过程,将转账异步化对于用户的感知基本没有影响
整个红包系统的容灾我们主要从接口限流,业务降级和多重机制保证状态机的推进这几个方式来进行的,下面对这几个方式分别介绍下:
接口限流接口限流是一种常见的容灾方式,用于保护系统只处理承受范围内的请求,防止外部请求过大将系统打崩。在进行接口限流前,我们首先需要和上下游以及产品沟通得到一个预估的红包发放和领取量,然后根据发放和领取量进行分模块地全链路的大盘流量梳理,下面是当时我们梳理的一个 b2c 全链路的请求量。