抖音视频的红包流程
抖音视频红包有两种类型,一种 B2C,一种 C2C。
B2C 活动,在春节红包雨活动中获得红包补贴 → 直接跳转相机 or 后续跳转相机录制祝福视频 → 录制视频后获得红包挂件 → 选择补贴 → 发放视频红包。
C2C 活动,录制视频后获得红包挂件 → 填写红包金额和数目、领取范围 → 点击发送红包后拉起收银台进行支付 → 支付成功后点击发布视频红包。
上面为发红包流程,而对于红包领取流程,B2C 和 C2C 的流程一样。
用户刷视频看到有视频红包 → 点击领取红包按钮 → 弹出红包封面 → 打开红包 → 查看到用户领取金额 → 看到详细页面。
抖音视频红包的问题
通用红包系统的设计
同时支持 B2C 和 C2C 两类红包。两类红包存在不同点,如,B2C 的红包发放需要通过使用补贴来发送,而 C2C 的红包发放需要用户去完成支付。B2C 的红包用户领取后需要去提现,而 C2C 的红包用户领取后直接到零钱。
同时不同类红包的状态机转换问题也是需要考虑的点。
大流量的处理
对于 B2C 红包,春节红包雨会有大量的用户进行访问。如何解决这类问题需要进行考虑。
稳定性容灾
在本次春节活动中,包括 B2C 和 C2C 两种业务流程,其中每个业务流量链路都依赖很多的下游服务和基础服务。在这种大型活动中,如果出现黑天鹅事件时,如何快速止损,减少对系统的整体影响,是一个必须要考虑的问题。
资金安全保证
在春节活动期间,B2C 会发放大量的红包补贴,如果补贴发生超发,或者补贴的核销出现问题,一个补贴被多次核销,将会造成大量的资损。另外 C2C 也涉及到用户的资金流入流出,如果用户领取红包后如果发现钱变少了,也可能会造成大量的客诉和资损。因此资金安全这块需要做好充足的准备。
全链路压测的进行
系统设计过程
理清系统操作
- 核心操作包括红包发送,领取,以及未领取的退款这三个操作
- 还会需要去查一些红包的信息和领取的信息等
- 对于发送,领取和退款这三个核心操作,需要维护所处状态
- B2C 特有的补贴的发放,也需要维护补贴的状态
在上面初步介绍红包系统后,可以看到红包的几个功能模块,有发放,领取,退款,补贴发放以及各种信息查询,另外还有状态机的维护等,对红包的功能进行梳理后,我们开始对红包的模块进行划分。
划分系统模块
模块划分原则
- 功能内聚,每个系统只处理一个任务,方便之后系统的开发和迭代,以及问题的排查
- API 网关层只进行简单的 proxy 处理
- 异步任务拆解
- 读写分离,将红包的核心操作和红包的查询分成两个服务
系统模块划分
- 红包网关服务
- HTTP API 网关,对外对接客户端和 h5,对内封装各个系统 rpc 接口,限流,权限控制、降级等功能
- 红包核心服务
- 主要承载红包核心功能,包括红包的发放、领取、退款,以及红包补贴的发放,维护红包状态机,红包的状态推进
- 红包查询服务
- 主要承载红包查询功能,包括红包详情、红包发送状态、红包领取状态、红包领取详情、红包补贴信息
- 红包异步服务
- 主要承载红包异步任务,保证状态机的流转,包括红包的转账,红包的退款,以及红包补贴的状态推进
- 红包基础服务
- 主要承载红包各个系统的公共调用,例如对 DB,redis、tcc 的操作,公共常量和工具类,相当于红包的基础工具包
- 红包对账服务
- 主要承载红包和财经的对账逻辑,按天和财经对账
整体架构
大流量补贴的发放处理
同步奖励发放
最初方案设计采用同步的补贴发放流程来处理。上游链路调用红包系统接口发券,发券成功后用户感知到券发放成功,可以使用该券来发放红包。
这种方式需要整个链路扛住活动期间总流量,并且都会打到数据库中。春节期间数据库资源紧缺。
异步奖励发放
整体流程改为通过 MQ 进行削峰,从而降低下游的流量压力,相当于是从同步改为异步,用户参与活动后会先下发一个加密 Token 给客户端,用于客户端的展示以及和服务端的交互处理。
相较于同步方案,弊端在于,红包补贴落库的时间晚,整个补贴的入账预估需要 10min,用户抢到补贴后无法立刻用补贴发送视频红包。
最终方案
在用户使用红包补贴进行视频红包发放时,我们会先对该补贴进行一个入库操作,入库成功后才可以用这个补贴进行红包发放。另外对于查询接口,我们无法感知到所有补贴是否完全入账,因此每次查询时我们都需要去奖励发放端查询全量的 Token 列表,同时我们还需要查询出数据库中用户的补贴,对这两部分数据进行一次 merge 操作,才能得到全量的补贴列表。
为了解决 MQ 异步会有延迟的问题,我们在用户进行请求时主动地进行入账,而用户主动的操作包括使用补贴发放红包和查询补贴,我们为什么只在补贴发放红包时入账而在查询补贴时不入账呢?因为用户的查询行为是一个高频行为,同时涉及到批量的操作,在操作 DB 前我们无法感知该补贴是否入账,所以会涉及到 DB 的批量处理,甚至用户每次来查询时我们都需要重复这个操作,会导致大量的 DB 资源浪费。而补贴的发放时入账则是一个低频的,单个补贴的操作,我们只需要在用户核销时入账即可,这样可以大量减轻数据库的压力,节省数据库资源。
红包领取方案的选型
悲观锁
在用户领取时对数据库的红包进行加锁,然后扣减金额,然后释放锁完成整个红包领取。
缺点很明显,用户过多时,行锁冲突。用户重试加重负担。
红包预拆分
在发红包时会对红包进行一个预拆分的处理,将红包拆成多个红包,这样就完成了锁粒度的细化,在用户领取红包时从之前的争抢单个红包锁变为现在多个红包锁分配。
这样将领取红包时问题 → 如何给用户分配红包。
一种常用的思路是当用户请求领取红包时,通过 redis 的自增方法来生成序列号,该序列号即对应该领取那一个红包。但是这种方式强依赖 redis,在 redis 网络抖动或者 redis 服务异常时,需要降级到去查询 DB 还未领取的红包来获取序列号,整体实现比较复杂。
最终方案
从业务场景分析,整个业务流程是用户拍摄视频发红包,然后在视频推荐 feed 流中刷到视频时,才会触发领取。相对于微信和飞书这种群聊场景,视频红包中同一个红包的领取并发数并不会很高,因为用户刷视频的操作以及 feed 流本身就完成了流量的打散,所以对于视频红包来说,领取的并发数并不会很高。
从业务实现角度分析,我们在用户领取完成后需要能获取到未领取红包的个数信息下发给用户展示,方案一获取红包库存很方便,而方案二获取库存比较麻烦。另外从系统开发复杂度和容灾情况看,方案一相对来说是一个更合适的选择。
同时,保护 DB 资源,减少锁的冲突,增加以下方案:
- 红包 redis 限流
- 为尽可能少的减少 DB 锁冲突,首先会按照红包单号进行限流,每次允许剩余红包个数*1.5 的请求量通过。被限流返回特殊错误码,前端最多轮训 10 次,在请求量过多的情况下通过这种方式来慢慢处理
- 内存排队
- 除了 redis 限流外,增加红包内存锁。对于单个红包,只有获得内存锁的请求才能继续请求 DB。将 DB 锁冲突迁移到内存锁冲突,而内存资源比 DB 资源廉价,同时可以轻松水平扩容。
- 内存排队的进一步改动
- 同一个红包请求发送到同一个 tce 实例,这里对网关层路由进行了调整,在网关层调用下游服务时,会按照红包单号进行路由策略。
- 在红包系统的 core 服务中基于 channel 实现了一套内存锁,在领取完成后会释放该红包对应的内存锁。另外为了防止锁的内存占用过大或者未及时释放,起了一个定时任务去定期地处理。
- 转账异步化
- 转账是一个耗时较长的操作,本身涉及和第三方支付机构交互,会有跨机房请求,响应延时较长,将转账异步化可以降低领取红包接口的时延,提高服务性能和用户体验。
- 从用户感知来看,用户更关注的是领取红包的点击开后是否领取成功,至于余额是否同步到账用户其实感知没那么强烈,另外转账本身也是有一个转账中到转账成功的过程,将转账异步化对于用户的感知基本没有影响。
稳定性容灾
接口限流
首先需要和上下游以及产品沟通得到一个预估的红包发放和领取量,然后根据发放和领取量进行分模块地全链路的大盘流量梳理,下面是当时我们梳理的一个 b2c 全链路的请求量。
有个各个模块的请求量后,汇总之后就可以得到各个接口,红包系统各个服务以及下游依赖的各个服务的流量请求,这个时候再做限流就比较方便了。
业务降级
当下游核心服务异常时,可能某一个链路就不可用,此时可以在 API 层直接降级返回一个比较友好的文案提示,等下游服务恢复后再放开。
比如在 C2C 的红包发送流程中,用户需要完成支付才可以发红包,如果财经的支付流程异常或者支付成功状态长时间未完成,会造成用户支付后红包发送不成功,也会导致前端来不停的轮训查询红包状态,导致请求量陡增,造成服务压力,甚至影响 B2C 的红包发放和查询。此时可以通过接口降级的方式,将 C2C 的红包发放降级返回,减少服务压力,同时降低对其他业务逻辑的影响。
对于非核心服务异常,可以降低用户部分体验的方式来保证服务的可用。比如用户在发 B2C 红包前需要先获取所有可用的红包补贴,我们会去奖励发放端查询到所有的 Token 列表,然后查询我们自己的 DB,然后进行 merge 返回。如果获取 Token 列表的接口异常时,我们可以降级只返回我们自己 DB 中的补贴数据,这样可以保证用户在这种情况下还可以进行红包的发放,只影响部分补贴的展示,而不是影响整个红包发送链路。
多重机制保证状态机的推进
如果某个订单长时间未到终态,比如用户领取红包后长时间未到账,或者用户 C2C 红包未领取长时间未给用户退款都有可能造成用户的客诉。因此需要及时准确地保证系统中各个订单的状态能推到终态。
- 回调。在依赖方系统订单处理完后会及时地通知给红包系统。但是,只依赖回调可能出现依赖方异常、网络抖动导致回调丢失。
- MQ。红包各个阶段都给红包系统发送一个 mq,间隔一定的时间消费 mq 查询依赖方的订单状态进行更新。
- 定时任务兜底。在定时任务多次执行仍未到终态的会 lark 通知,及时人工介入发现问题。
资金安全保证
交易幂等
红包发放/领取/退款请求时,基于订单号的唯一键来保证接口幂等性。
同时,红包补贴发放接口是幂等的,外部同一个单号多次请求发放补贴,需要保证只会发一张券。
数据库唯一键冲突来解决接口幂等是最可靠的。但是需要注意数据库存在分片实例时,如果不是基于唯一键来进行分片,则可能存在唯一索引失效的情况。
抖音在业务系统的设计中,是按照 uid 分片的方式来建立业务的数据库表,这就导致补贴的分片键是 uid,虽然也设置了红包的补贴单号作为唯一键。但是其中存在一个风险就是如果上游的系统调用补贴发放时,同一个外部单号更换了 uid,就可能会导致两个请求分别打到不同的数据库实例上,导致唯一索引失效,造成资损。
为了解决这个问题,又额外的引入一个以补贴发放外部单号作为分片键的数据库来解决这个风险。
B2C 红包核对
在 B2C 链路中,整个链路主要是从补贴发放到红包领取,我们对这几个链路的上下游的数据都进行相应的小时计 hive 对账。
C2C 红包核对
C2C 链路中,整个主要从用户发起支付,到用户领取转账以及最后红包过期退款。在支付,转账,退款这三个流程都需要进行相应的核对。同时,还需要保证用户的红包发放金额大于等于红包转账金额+红包退款金额,这里大于等于是因为红包从发放成功到退款成功整个周期会在 24h 以上,另外可能存在转账在途的这种订导致会有多笔退款单,如果要求严格等于的话具体对账时机没法控制。
系统压测
首先需要对整个压测链路整个改造,和上下游沟通是否可以压测,不能压测的需要进行相应的 mock 处理。另外对于存储服务,数据库,redis 和 mq 都要确保压测标的正确传递,否则可能会影响到线上。
B2C 红包链路压测
首先是补贴的发放,然后通过查询补贴,通过补贴来发放红包,为了模拟多人来领取的情况,起了多个 goroutinue 来并发的领取红包。
C2C 红包链路压测
在压测流程中,因为涉及到外部系统的依赖,如果等待全链路 OK 时再一起压测可能会导致一些未知的问题出现。因此我们需要自己压测没问题后再开始全链路一起压测,在图中和支付相关的蓝色模块我们都添加了相应的 mock 开关,来控制压测的结果。在 mock 开关打开时,会直接构造一个结果返回,在 mock 开关关闭时,会正常地去请求财经获取结果。
后续规划
服务 set 化
略,类似于 『红包系统』微信红包系统设计。