字节跳动开放平台-钱包团队全面负责字节跳动八段2022年春节活动奖励链接的记录、展示和使用。以下是本次工作的介绍和总结。首先总体介绍一下业务背景和技术架构。然后阐述了每个难点的具体实施方案,最后进行了抽象总结,希望能够为后续活动提供指导。
1. 背景挑战目标
1.1 业务背景
(1)支持八段:字节产品2022年春节活动需支持八段APP产品(包括抖音/抖音火山/抖音快车版/西瓜/头条/头条快车版/番茄小说/番茄听)奖励兑换。用户可以在上述任一终端参与活动,获得的奖励可以在其他终端提取和使用。
(2)玩法多样:主要包括集卡、好友页红包雨、红包雨、集卡抽奖和烟花燃放等。
(3)奖励多样:奖励类型包括现金红包、补贴视频红包、商业广告优惠券、电商优惠券、支付优惠券、消费金融优惠券、保险优惠券、信用卡优惠券、茶券、电影票券、豆+优惠券、抖音文创优惠券、头像挂件等
1.2 核心挑战
(1)设计大流量解决方案,实现八端奖励核算和展示的互操作性。预计最高奖励为360 万QPS。
(2)多种奖励发放场景,玩法多样;奖励的种类很多,一共有十多种奖励。连接到多个下游系统。
(3)从奖励系统稳定性、用户体验、资金安全、基础运营能力等角度保障活动顺利进行。
1.3 最终目标
(1)奖励入口:设计并实现八端奖励互通奖励入口系统,连接多个奖励下游系统,平滑不同奖励下游的差异,对上游屏蔽底层奖励入口细节,设计统一的接口协议提供上游业务。提供统一的错误处理机制、幂等记账能力和奖励预算控制。
(2)奖励展示/使用:设计并实现活动钱包页面,支持用户收到的奖励在八个终端上展示,支持用户查看、提取现金(现金)、使用优惠券/小工具等。
(三)基本能力:
【基础SDK】提供查询红包余额、累计收入、用户是否在春节活动期间获得奖励等基础SDK,供业务方查询。
【预算控制】对接上游奖励发放端的算法策略,实现大流量卡券账户的库存控制能力,防止超额发放。
【提现控制】跨年夜多轮奖励发放后,将为用户提供灰度提现能力以及提现时账户尚未记录的处理能力。
【运营干预】活动页面灵活的运营配置能力,支持公告的快速发布和用户的及时触达。为了应对黑天鹅事件,支持批量补发卡、红包的功能。
(4)稳定性保障:在大流量记账场景下,保证钱包核心路径的稳定和完善,通过资源扩容、限流、熔断、降级、覆盖等常见的稳定性保障手段,保证用户奖励——向上、资源隔离等方向的核心经验。
(5)资金安全:在大流量入账场景下,通过幂等、对账、监控和报警机制,保证资金安全,保证用户资产得到充分分配,而不是少分配。
(6)活动隔离:实现内测活动、灰度活动、官方春节活动三个阶段奖励记录和展示的数据隔离,互不影响。
2. 产品需求介绍
用户可以在任意一端参与Byte的春节活动以获得奖励。以抖音红包雨现金红包入账场景为例,具体业务流程如下:
登录抖音参与活动活动钱包页面点击提现按钮进入提现页面进行提现提现结果页面。另外,您还可以从钱包页面进入活动钱包页面。
奖励分配的核心场景:
收藏卡:抽取收藏卡时将赠送各种卡片和优惠券。收藏卡锦鲤还将送出大额现金红包。收藏卡抽奖期间,瓜分奖金和优惠券;
红包雨:发放红包、优惠券和视频补贴红包,其中红包和优惠券最高分别为1.80w QPS;
烟花汇演:发放红包、优惠券、头像挂件。
3. 钱包资产中台设计与实现
2022年春节活动中,UG主要负责活动玩法的实现,包括集卡、红包雨、烟花燃放等具体活动相关的业务逻辑和稳定性保障。钱包方向定位是在高流量场景下实现奖励进入、奖励展示、奖励使用和资金安全相关的任务。资产中台负责奖励分配和奖励展示。
3.1 春节资产资产中台总体架构图如下:
钱包资产中台核心系统划分如下:
资产订单层:汇聚八端奖励账户链路,提供统一接口协议连接UG、激励中台、视频红包等上游活动业务方的奖励发放功能,同时屏蔽上游对接奖励业务下游的逻辑处理,支持预算控制、补偿、订单数幂等等。
活动钱包api层:汇聚8个终端奖励展示链接,同时支持大流量场景
3.2 资产订单中心设计
核心发行模式:
阐明:
活动ID唯一区分一个活动。这个春节被分配了一个单独的家长活动ID。
场景ID与具体的奖励类型一一对应,定义了该场景下发放奖励的唯一配置。场景ID可配置的能力包括:发放奖励票据副本;是否需要赔偿;限流配置;是否进行库存控制;是否需要对账。为可选业务访问提供可插拔功能。
实现效果:
实现不同活动之间的配置隔离
每个活动的配置都是树形结构,允许一个活动发放多个奖励,一种奖励发放多个奖励ID。
一个奖励ID可以有多种分配场景,支持不同场景的个性化配置。
订单号设计:
资产订单层支持订单号维度的奖励幂等性。订单号的设计逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},保证不超级发放,每个场景用户最多只能获得一次奖励。
4、解决核心难点问题
4.1 难点一:支持八端奖励数据互通
上面已经介绍了背景。参加2022年春节活动的产品终端共有8个。其中,抖音和今日头条APP的账户体系不同,无法通过用户ID开启奖励互通。具体解决方案是,字节账户中心打通八终端账户体系,为每个用户生成唯一的actID(手机号码优先级最高,如果不同终端登录的手机号码相同,不同终端的actID一致)。基于字节账户平台提供的唯一actID,钱包方设计并实现了支持八端奖励的进入、查看和使用的通用解决方案。即每个用户的打赏数据与actID绑定,录入和查询都是通过actID维度实现的,可以实现八终端打赏互通。
示意图如下:
4.2 难点二:高场景下的奖励入账实现
寻找金红包是每届春节活动中最关键的环节,今年也不例外。原因有以下几个:
预计现金红包最大流量为1.80w TPS。
现金红包本身价值较高,需要保证资金安全。
用户对现金非常敏感,因此在保证用户体验和功能完整性的同时还必须考虑成本问题。
如上所述,发现金红包面临着比较大的技术挑战。
发红包实际上是一种交易行为。资金流向是从公司成本出发,然后进入个人账户。
(1)技术方案是支持订单号维度的幂等性。对同一订单号的多次请求只会被记录一次。订单号生成逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从订单号设计层面保证不会超发。
(2) 为了支持高并发,传统的解决方案有以下2种:
具体计划类型
实施思路
优势
缺点
同步入账
申请与预估流量相同的计算和存储资源
1、开发简单; 2、不易出错;
浪费存储成本。以账户数据库为例,实际压测结果显示,支持30万个红包需要152个数据库实例。如果支持180万个红包,不计算tce、redis等其他计算和存储资源,至少需要1152个数据库实例。
异步录入
在申请某些计算和存储资源时,实际记录容量与预估存在一定差异。
1、开发简单; 2、不易出错; 3、不浪费资源;
用户体验受到很大影响。记录帐户有很大的延迟。以今年的活动为例,就会有十几分钟的延误。用户参与游戏并获得奖励后,在活动钱包页面看不到奖励,也无法提现。会出现大量的客户投诉,影响抖音活动的效果。
上述两种传统技术方案都存在明显的缺陷。那么我们思考一下,有什么方案既可以相对节省资源,又能保证用户体验呢?最终的解决方案是红包雨代币解决方案。具体的解决方案是使用异步签入加上更少量的分布式存储和更复杂的解决方案来实现的。下面我们将详细介绍。
4.2.1 红包雨 token 方案:
本次春节活动红包雨/集卡抽奖/烟花燃放期间流量红包发放量巨大。如前所述,奖励分发的最大QPS预计为180万QPS。根据现有的账户入口设计,大量的存储和计算资源支持,根据预计发放的红包数量/产品最大可接受的发放时间,计算出钱包必须的最低TPS支持实际支付30万,所以实际发行时有一个压单的过程。
设计目标:
在预估分配给用户(18万)与实际账户(30万)差距较大的情况下,保证用户的核心体验。用户在查看和使用前端页面时无法感知到订单的按下过程,即不会影响查看和使用体验。相关显示数据包括余额、累计收入和红包流量,用途包括提现等。
具体设计方案:
每次我们在高流量场景下向用户发送红包时,我们都会生成一个加密令牌(采用非对称加密,包含红包的元信息:红包金额、actID、发放时间等)。 ),分别存储在客户端和服务器端(容灾和互备),每个用户都有一个token列表。每次发红包时,代币的入账状态都会记录在Redis中,然后用户在活动钱包页面看到的现金红包流量、余额等数据是记账红包列表+代币的组合list - 计算/输入的令牌结果列表。同时,为了保证用户提现体验无感知压红包过程,在进入提现页面或点击提现时,强制进入未记账代币列表。这样保证了用户提现时账户的余额是应记账的总金额,并且不会阻塞用户的提现过程。
示意图如下:
代币数据结构:
token使用pb格式,单次测试验证存储消耗实际上是使用json的2倍,节省了请求网络的带宽和存储成本;同时,序列化和反序列化的CPU消耗也减少了。
//红包雨代币结构
类型RedPacketToken 结构体{
AppID int64 `protobufvarint,1,opt jsonAppID,omitempty ` //终端ID
ActID int64 `protobufvarint,2,opt jsonUserID,omitempty ` //ActID
ActivityID string `protobufbytes,3,opt jsonActivityID,omitempty ` //活动ID
SceneID string `protobufbytes,4,opt jsonSceneID,omitempty ` //场景ID
amount int64 `protobufvarint,5,opt jsonAmount,omitempty ` //红包金额
OutTradeNo string `protobufbytes,6,opt jsonOutTradeNo,omitempty ` //订单号
OpenTime int64 `protobufvarint,7,opt jsonOpenTime,omitempty ` //抽奖时间
RainID int32 `protobufvarint,8,opt,name=rainID jsonrainID,omitempty ` //红包雨ID
Status int64 `protobufvarint,9,opt,name=status jsonstatus,omitempty ` //计费状态
}
令牌状态机流程:
在主叫账户实际记入之前,它会被设置为处理(2)状态。如果呼叫账户成功,则会被设置为成功(8)状态。不存在发红包失败的情况,后续尝试都可以成功。
代币安全保障:
采用非对称加密算法,尽可能保护数据库存储的客户端不被破解。加密算法是限制他人访问的秘密仓库。同时考虑到极端情况下,如果token加密算法被黑手破译,则可以监控检测到报警,并可以对系统进行降级。
4.2.2 活动钱包页展示红包流水
需求背景:
活动钱包页面显示的红包流量是现金红包入账流量、现金提现流量、C2C红包流量三个数据源的合并。按照创建时间倒序排列。需要支持分页,并且可以降级,保证用户体验感知不到放置金红包的过程。
4.3 难点三:发奖励链路依赖多的稳定性保障
降级红包发放流程示意图如下:
根据历史经验,实现的功能越复杂,依赖关系就会越多,相应的稳定性风险就会越高。那么如何保证高度依赖的系统的稳定性呢?
解决方案:
现金红包记录最基本的功能是记录用户收到的红包,同时支持幂等性和预算控制(避免超发)。红包账户的幂等设计强烈依赖数据库来维持交易一致性。但如果出现极端情况,中间环节可能会出现问题。如果是弱依赖,需要在不影响主分发流程的情况下降级。钱包方向发送红包的最短路径是依靠服务实例计算资源和MySQL存储资源来实现现金红包。
红包分发的强度取决于梳理图标:
PSM
相关服务
是否有很强的依赖性?
降级计划
降级后的影响
资产中台
TCC
是的
降级读取本地缓存
没有任何
拜特克夫
不
主动降级切换,跳过bytekv,依赖下游幂等
没有任何
资金交易层
分布式锁Redis
不
被动降级,调用失败,直接跳过
基本没有
代币Redis
不
主动降级切换,无需调用Redis
用户可以感觉到到账有延迟,并且会出现很多客户投诉。
MySQL
是的
如果您对所有者有任何疑问,请联系DBA 更改所有者。
故障期间无法领取红包
4.4 难点四:大流量发卡券预算控制
需求背景:
春节活动期间,烟花燃放将于除夕夜7点30分开始,是发券集中、人流量大的场景。钱包端配合算法策略控制卡券发行库存,防止超发。
执行:
(1)钱包资产中心维护每张卡和优惠券模板ID的消费和发行金额。
(2)每张卡发卡前,算法策略都会读取钱包SDK,获取卡模板ID的消费和总库存。同时,也会设定一个门槛。若优惠券剩余金额低于10%,将不发放优惠券(使用安全券或祝福保证安全)。
(3)同时钱包资产中心在优惠券发放过程中累计每个优惠券模板ID的消耗量(使用Redis incr命令原子累加消耗量),然后与总活跃库存进行比较。如果消耗大于总库存,那么拒收防止超发也是一个彻底的过程。
具体流程图:
优化方向:
(1)大流量下使用Redis计数时,单个key会出现热键问题,需要通过拆分key来解决。
(2)大流量场景下操作Redis会出现超时问题。返回上游进行处理。如果上游继续重试优惠券发放,则会消耗更多库存,发放更少。本次春节活动实际活跃库存在预估库存基础上增加了5%。幅度来缓解超时导致的发行量低的问题。
4.5 难点五:高 QPS 场景下的热 key 的读取和写入稳定性保障
需求背景:
烟花燃放活动将于除夕夜7点30分开始,实时显示所有红包及烟花燃放红包的累计总数。最大读取流量估计为180wQPS,写入最大流量为30wQPS。
这是典型的超大流量、热键、更新延迟不敏感、数据一致性不强(数字总是累加)的场景,容灾和降级处理必须同时进行。最终,实际活动中显示的金额与预期产品发行值误差在1%以内。
4.5.1 方案一
提供SDK接入方式,复用主会场机实例资源。要在高QPS下读写单个key,更容易想到使用Redis分布式缓存来实现。但是,读取和写入单个密钥将在一个实例上完成。压测单实例的瓶颈是3w QPS。所以我们做的一项优化就是拆分多个key,然后使用本地缓存来解决这个问题。
具体编写过程:
设计是拆分100个key,每次发红包时根据请求的actID%100使用incr命令累加数量。因为不能保证幂等性,所以超时后不会重试。
阅读过程:
与写入过程类似,首先读取本地缓存。如果本地缓存值为0,则将每个Redis的键值读取并累加在一起后再返回。
问题:
(1)拆分100个key会造成读扩散问题,需要申请更多的Redis资源,存储成本会比较高。而且,可能存在读取超时问题,不保证一次读取所有key都成功,所以返回的结果可能比上一次要少。
(2) 在灾难恢复计划方面,如果您申请
备份 Redis,也需要较多的存储资源,需要的额外存储成本。