大流量活动下钱包提现方案的设计与实现 <a href="https://www.51cto.com/original.html" target="_blank" class="article-type" data-v-5614a5b8>原创</a>
2022-04-25 16:57:50来源:字节跳动技术团队
本文主要从服务端角度针对 2022 年春节 Flower 活动中钱包提现模块做一下总结与反思,希望可以对整个开发过程中使用的技术和遇到的问题进行整理和沉淀,在后续类似的活动中可以产生一些帮助。
一、活动背景与交互流程2022 年春节活动目标是在抖音、火山、西瓜等八端启动,希望抖音端能够给多端进行导流,实现“同一个字节,同一个春节”活动。对用户来说,可以在任意一端参与春节活动并在钱包中看到集卡、红包雨等玩法获得的所有收入,最终可以在任一端提现春节收入至个人账户中,保证用户在活动中的奖励能够落地,提升用户春节活动参与度。
交互流程用户在进入活动钱包页后可查看参与活动获得的奖励收入,点击【去提现】按钮可以跳转到提现页面。在提现页面用户输入提现的金额并选择已绑定的到账方式, 然后点击【确认提现】即可提现活动收入到自己选择的个人账户中。
二、大流量下的主要问题提现即用户将自己通过参与集卡、红包雨等活动玩法所活动的奖励收入提取至用户的银行卡、支付宝或抖音零钱等个人账户中。由于春节活动存在集中开奖导致的高流量,活动发奖瓜分金额巨大等特点,在开发春节活动提现的过程中,有几个方面需要重点考虑:
1. 入账延迟与提现限制除夕当晚 19 点到 23 点每个整点开放红包雨,19:30 春节活动集卡开奖与烟火大会启动。此时面对上百万 QPS 的用户奖励入账,可能存在部分请求入账存在延迟,导致用户在提现的时候看到的金额与活动参与获得奖励金额不一致的情况。
此外今年春节活动提现增加了每笔订单 1 元起提的门槛限制,在除夕晚上集卡开奖、红包雨和烟火大会多个玩法的加持下,用户很容易获得 1 元以上的收入。但如果在获得收入达到门槛后立即提现,可能会导致参加后续玩法获得奖励较少而无法提现的情况。
2. 高并发集卡开奖与红包雨后,提现入口打开时将面对几十万的请求流量,经过用户选择到账方式和输入提现金额后也有数万 QPS 的提现下单请求。钱包服务端收到请求后会操作扣除用户的活动账户余额,接着调用财经侧(字节内部的支付中台)请求出款,财经侧维护与各支付机构(支付宝、银网联)的接入交互。但各支付机构分配给各单位的出款请求流量有限额,字节这边获得的容量与提现出款相差了一个数量级。此时需要在保障用户的提现体验不受影响的同时,又能够确保下游渠道侧不会因流量较高导致可用性抖动。
3. 资金安全提现是春节活动的最后一道流程,公司在用户的春节活动收入账户进行扣款并将资金通过预先设置的备付金账户转入至端上绑定的个人账户中,从而使获得的奖励最终落地。如果用户在端上的操作出现打款超额等情况,一旦出款成功则基本不会有追回的可能,因此,资金安全是提现业务开发过程中必须考虑并保证的部分,确保每笔出款有迹可循且符合提现规则。
三、设计方案为解决上述问题,我们通过 RocketMQ 进行异步出款来保证用户体验,同时 RocketMQ 的使用还可以对银行卡等出款渠道进行削峰来减少下游的过高流量。在资金安全方面,每笔订单在进行春节活动收入账户扣款和现金出款时做了幂等操作,并增加对账任务对所有流水进行对账校验。
1. 延时放量除夕当晚从 19:30 集卡活动开奖用户进入主会场即可看到集卡奖励,同时烟火大会开启参与活动即可获得红包奖励,此外从 20:00 开始到 23:00 每个整点都会有红包雨,用户会不断获得春节活动奖励并进入钱包页查看个人收入。
为保证用户集卡开奖和红包雨活动入账顺利,不会出现看到奖励但钱包中无收入或收入不足导致提现错误的问题,我们在用户进入提现页面的时候会根据端上请求参数中的红包 token 列表进行一次入账请求,以确保用户在确认提现下单前账户中的金额如果没有完成入账的话可通过 token 列表进行一次强制入账,但为给用户在提现页面有较好体验,此处的入账为弱依赖请求。当用户确认提现下单的时候,我们设置了强依赖性的强制入账作为最终兜底方案,来使得最终提现扣款时奖励金额已经入账成功并支持提现。
同时,为保证用户可以在活动结束后提现参与获得的所有奖励,减少因提现门槛导致用户参与后续玩法获得奖励较少而无法提现的情况,我们在春节活动中开启了延时放量提现。从 19 点到凌晨 1 点之间关闭提现入口,用户只能在主会场参与活动获得奖励,而无法进入钱包页面中进行提现操作。当用户在活动钱包页点击【去提现】时,会弹窗提示用户在 2 月 1 日 01:00 后可提现。此外,在弹窗中我们也加入了对用户的绑卡营销策略,引导用户预先绑卡,提现快人一步。
随着凌晨一点提现入口打开,可能会有大量用户涌入提现页面进行提现。此时,为防止瞬间流量突增过高可能引发数据库连接问题,我们通过在配置平台上进行配置,针对用户 id 进行取模后的结果进行分批放开。在有限的情况下,确保用户入账无误,请求不被限流是我们用户体验是否良好一个比较重要的评判因素。延迟到凌晨 1 点分批次放开提现有效降低了用户提现的并发,保障了用户提现体验。
2. MQ 异步出款在除夕当天的晚上 19:00 到春节凌晨 01:00 时间段内,春节活动钱包页中会暂时关闭提现功能,进行部分营销导流。而随着凌晨 01:00 提现开关打开,请求会蜂拥而至逐步上涨至数万 QPS,但由于银网联的处理能力有限,导致银行卡渠道出款最高可支持的 QPS 只有几千。此时如果提现模块不进行限速下单的话,可能存在下游系统被压垮引起雪崩的风险,同时用户会给感受到提现功能卡顿并频繁失败。
为解决该问题,我们引入了 RocketMQ 来进行异步出款。当用户在钱包页进行提现操作时,服务端会在春节活动收入账户扣款完成后立即返回结果并跳转至提现结果页面展示当前状态,同时将当前请求参数发送至 MQ 中进行异步消费出款。这样给用户的感觉即账户余额已扣除,提现出款进行中,稍后也可以通过账单流水查询提现结果。
将消息发送到 MQ 后,提现模块利用 MQ 消费提现订单的现金出款,通过下游消费者有限的消费能力进行消息处理。同时增加自定义限流器对每个出款渠道进行限流,利用 MQ 进行流量削峰与限流出款两种方式双重保证了下游出款不会因流量过高而出现抖动。当消费成功时则顺利出款,当消费失败或被限流时则返回错误,MQ 会进行消费重试。我们在这里设置 MQ 最大重试次数为 3 次,如果消息没有超过最大重试次数,则被放入 retry 队列;如果消息达到最大重试次数,则放入死信队列不再处理。
2.1 定时任务为防止提现订单因 MQ 多次重试消费失败或其他原因导致状态一只卡在某个中间状态停止更新,我们额外设置了定时任务进行补单操作推进提现状态。每小时固定从数据库中捞取已被创建超过 4 个小时且当前还处于未完成状态的订单,并根据其当前状态进行推动:
待扣款的订单,则说明用户的账户收入还未进行扣款,此时则直接将订单状态推进为失败状态;待出款状态的订单,请求财经接口进行出款操作,推动状态到出款中或出款完成;出款中的订单,查询财经出款订单的状态,如果财经侧已成功或失败则将该状态同步更新到提现订单中,如果财经侧查单不到的话则调用财经出款接口进行重试;对于从任一状态流转至失败的订单,我们会查询账户的订单流水,如果账户侧存在余额扣减流水的话,则操作进行余额退回,保证失败的订单不会扣减用户的收入。3. 提现资金安全在提现的过程中,一旦技术方案设计有问题,容易存在资金安全问题:账户未扣款但现金已转入用户的个人账户,账户多次扣款或者现金多次出款等。因此,在春节活动中提现模块的设计中,资金安全问题是重点考虑的部分。在提现请求发生时,服务端需要确保每笔订单一定对应一次账户余额扣减,一次现金出款。而提现完成后,需要有对账任务与账户和财经出款进行对账,分别对提现订单的金额和状态进行校验,保证事件中的验证无误。
3.1 订单幂等幂等,指任意多次执行所产生的影响均与一次执行的影响相同。提现针对 orderID 做幂等性控制,在账户侧每个 orderID 只有一笔扣款操作,从而保证用户的活动账户余额不会被重复扣款;同时,在用户当前订单提现失败后进行账户余额回滚操作时,首先查询账户侧是否存在扣款订单,如果存在则进行余额退回,退回时控制一笔扣款操作对应一笔退回流水,防止出现多退的情况。
账户完成扣款之后,需要调用财经的出款接口将资金从公司预先设置的备付金账户转入至端上绑定的个人账户中,此时需要确保每笔提现请求只能有一次出款。在每次操作提现订单进行现金出款时,我们使用 redis 分布式锁对 orderID 进行加锁操作,加锁成功后判断当前订单状态,如果是待出款状态则调用财经接口进行现金出款。在接口调用后立即更新订单状态为出款中,防止重复调用引发可能出现的重复出款操作。同时,财经侧也针对 orderID 做了幂等控制,确保每笔 orderID 都对应一笔出款。
3.2 对账校验涉及到资金流动,需要有对账任务来保证上下游之间资金数据的一致性,能够及时发现处理金额或状态差异导致的资损问题。我们在对账平台分别增加了准实时对账和天级对账来进行资金的校验。
准实时对账
在提现事件发生过程中,我们在对账平台中增加了与下游服务(账户、财经)提现数据的准实时对账,确保提现订单每次状态变化时都是准确无误的:
1. 与账户侧准实时对账:
a. 在余额扣减成功后账户侧会保存一笔提现扣款的数据流水,此时需要将扣减流水与提现订单进行金额和状态校验,确保扣款状态和金额一致;
b. 在提现失败的时候,如果此时账户侧已经扣款成功的话则需要将之前扣减的金额退回至用户的活动账户中,此时需要在余额退回成功后进行账户金额退回流水与提现订单状态、金额做校验。
2. 与财经侧准实时对账:
a. 在提现订单状态更新为成功或失败时,获取财经侧的出款订单数据与提现订单数据进行一致性校验,判断双方数据的状态、金额是否一致。
对账平台中的数据校验,是基于数据双方的 binlog 消费进行的准实时对账,在对账双方任一方缺失数据或双方对账状态金额出现不一致的情况下便会发送飞书报警通知。
此外,我们还在线上服务中增加了自主对账任务,通过消费提现数据库的 binlog 消息。针对其中到达终态的订单,我们会根据订单状态分别通过接口调用的方式对账户、财经侧进行查单检查。成功的提现订单在账户的查单接口中可以查到一笔扣款流水,在财经的查单接口中也会有一笔出款成功的订单。而失败的提现订单在账户的查单接口中可以查到一笔扣款和一笔退回余额的流水,在财经的查单接口中如果可以查到订单的话则只有失败订单,如果没有订单的话说明提现流程还没有走到出款便失败了,此时可忽略缺失的差异。
天级对账
另外,我们也增加了与下游服务(账户、财经)的天级对账,作为准实时对账之外的一种兜底对账。因为上下游之间可能因调用失败或者回调失败导致状态同步不及时,我们增加了定时任务进行订单状态推进,保证每笔提现订单最终都可以达到终态。天级对账即为了解决状态同步不及时可能引发的准实时对账差异,通过每天生成的 hive 数据进行状态和金额校验,减少时间差产生的误报。
四、前期预案为保证活动上线后用户能够在钱包页中进行正常提现,我们在活动开始前增加了准备预案。
1. 提前演练在活动正式开始前,我们组织了内部圈定人群、内部所有人、外部圈定城市等三次预演,针对春节活动红包雨和现金提现进行了提前演练,模拟春节活动的正常流程与突发情况的处理。通过演练,我们可以提前发现整个活动流程是否顺利,并将可能存在的问题提前暴露解决。
2. 充分压测为支持春节活动过程中产生的大流量请求,保证给用户提供良好的活动体验,我们将春节活动现金提现与钱包日常收入提现的功能进行了集群隔离。在代码开发上线后,申请压测资源对各业务流程进行了预估流量压测,集群隔离也使得压测操作不会对线上正常业务有任何影响。
在提现业务压测的过程中,有两个方面需要做一些数据准备:
查询到账方式接口需要对压测的 uid 构造已绑定的到账方式结果返回;确认提现下单接口需要对压测的 uid 有绑定到账方式的同时,还需要 uid 对应的活动账户中有足够的金额支持余额扣减。为解决此问题,财经的同学在压测之前提供了一批已绑定到账方式的测试 uid 生成的文件, 方便我们在进行到账方式查询接口压测的时候能够从文件中指定 uid 参数。另外,在账户同学压测入账接口的时候,我们提供了该文件让其帮助对这批 uid 进行入账。如此在压测提现下单的时候,我们使用已经绑定到账方式的测试 uid 数据,其活动账户中已经存在余额可支持提现余额扣减。
最终,通过对到账方式查询、确认提现下单等接口进行全链路压测后,我们能够准确评估了为支持春节活动最高 QPS 所需的各项资源容量,使春节活动可按照预先计算的流量支持用户操作提现。
3. 除夕当日执行剧本除夕是我们春节活动启动后的重要时间点,当天有多场红包雨,同时还有烟火大会和集卡开奖等玩法出现,整个活动会在春节联欢晚会开始的时候达到高潮。在这种大型活动参与过程中,每个人或多或少都会有一些压力在身上。纵使代码经过验证,前期进行过多次演练均无问题,但还是需要抱有谨慎的态度来应对重要活动的开始。在大脑记忆力有限的情况下,为防止出现遗漏,我们针对除夕当天写了执行剧本:
执行细节。从除夕上午十点开始到初一凌晨两点,每场红包雨前需要做什么准备,红包雨发生时需要查看哪些监控指标,红包雨后是否需要记录数据等,执行剧本需要详细记录每个时间点需要做的事情。配置校验。提现业务在春节活动上有活动配置与限流等。在活动开始前需要再次做一遍检查,确保各项配置和限流均正确无误。容灾方案。除执行细节和配置校验外,我们还在剧本中加入了容灾预案,方便在某项流程出现问题的时候能够及时根据预案进行处理。交叉检查。剧本中的各项操作细节和配置检查均为两个人分工进行,通过交叉检查的方式防止出现一人疏忽大意而错误改动的情况。五、活动总结春节活动上线后,用户积极参与各种玩法并在其活动钱包中进行现金提现。在除夕当晚,延时放量虽然使用户在获得收入的第一时间不能进行提现,但用户奖励入账正常,延时放开后提现请求没有被出款渠道限流,有效地保障了用户提现体验。同时,在渠道侧出款能力有限的情况下,通过使用 MQ 进行异步出款有效地限制了对下游服务的请求流量,使其没有因流量过高而导致出款异常。
此外,在春节活动的整个时间段内,通过对提现流程进行风险梳理,增加对账平台准实时与小时级对账支持和线上服务对账支持,双重保障了春节活动现金提现模块对账任务的全覆盖,使用户在参与活动并收获奖励进行提现的过程中未对其造成资金损失,确保了用户的参与度。