B站票务系统抢票下单流程设计与优化实践
B站票务系统抢票下单流程设计与优化实践
随着B站票务业务的快速发展,特别是在2022年之后漫展市场的热度激增,传统的同步事务处理方式已经无法满足高并发场景下的性能需求。本文详细介绍了B站票务系统抢票下单流程的三阶段优化历程,从最初的同步事务处理到异步下单,再到基于Redis的库存优化方案,最终实现了系统的稳定性和性能的大幅提升。
1. 背景
B站票务业务目前覆盖全国绝大部分2次元及2.5次元的展览、演出等项目。例如,B站自己主办的BiliBili World和各种协作承办的漫展等。在业务形态上,提供了多种服务:
- 抢票功能:帮助用户抢购热门活动票,避免黄牛抢购风险场景发生。
- 电影票:提供便捷的电影票购买通道,接通站内电影营销。
- 选座功能:支持用户自主选择座位。
- 检票工具:提升现场入场效率,体现平台服务独特性。
- 结算:业内高效、快速对账结算体系。
这些服务为用户提供便捷的票务体验,涵盖了从购票到入场到结算全流程需求。
2. 目标
对于一个热门的抢购项目,用户的基本访问链路为:
项目列表/收藏 ==> 商品详情 ==> 选中对应的场次以及票种 ==> 结算页补充下单信息 ==> 下单。
在上述链路中,其中项目列表以及商详基本都是一些固定信息(商详页面的是否售完,用户也可以接受短期的延迟),所以这部分流程基本可以通过缓存去做处理。而大部分用户在结算页补充完个人信息之后,会对于创单接口进行多次且频繁的访问。下单接口流量大、实时性要求高,且直接影响交易收入转化,所以保障下单接口稳定很重要。
围绕流量峰值承载、数据库压力优化、用户等待时长缩短三大核心目标,我们对下单链路进行了三阶段迭代。
第一版 - 同步事务处理
在票务从0-1的过程中,为了增加市场的占有率,需要实现功能的快速迭代,所以对于方案的设计要以基础功能为主。
方案
流程逻辑:用户请求实时同步处理库存扣减与订单写入。
效果
初始版本过程中,足以应对普通流量下的用户下单需求,但如果是抢购的场景,大事务以及库存DB的单行扣减问题的会影响到接口的性能以及接口响应时间,甚至出现库存死锁等问题。从而导致服务的雪崩。
- 高并发下大事务阻塞——DB连接池耗尽,响应延迟飙升。
- 单行库存热点——InnoDB行锁竞争引发死锁,扣减失败率超30%。
- 无弹性扩展能力——服务雪崩风险显著。
第二版-异步下单 - 异步削峰
针对于抢购的场景,为了避免出现因库存以及大事务场景下影响用户下单,衍生出了异步下单的版本:
方案
流程逻辑:解耦请求接收与事务执行,引入异步分批处理。
- 前端交互:用户获取下单Token,轮询查询结果(平均等待5-8秒)。
- 后端处理:
- 库存批量冻结:合并SQL减少DB操作频次。
- 优惠券并行校验:拆分耦合模块提升吞吐量。
针对于用户下单接口中的容易导致DB出现异常的模块,进行异步削峰处理==>将用户下单请求以及实际下单接口,拆分为两个模块处理,并且将下单批量化处理,减少DB的操作次数,降低DB的风险。
此时用户下单分成了三个步骤:
- 用户下单,获取下单标识(唯一token)
- 定时任务,批量下单,将之前的单条库存扣减、订单插入修改为进行批量冻结库存,并行冻结优惠券,批量合并sql插入数据库,最大限度上减少性能消耗
- C端在下单页新增轮询接口根据唯一token轮询下单结果
效果
此方法很大程度上解决了数据库压力问题,但由于前端沦胥,用户体验感下降,因轮询导致阻塞等待。
第三版-redis缓存扣减库存下单
异步下单已经能够解决大部分的热门项目的抢购问题,但是从2022年之后,漫展的市场热度出现了巨大的变化,在抢购之前预估流量为平时流量的2倍,然而实际结果是比预估的流量大了10倍不止,紧随其后的BW的抢购更是说明了异步下单已经不能保证一些热门项目的抢购, 对于下单接口需要进一步的改造。
在之前的异步下单链路中还是存在几个问题
- 前端轮询下单结果,会有一个较长时间的等待。用户体验感差
- 支付回调流量不可把控,如果支付回调QPS过高,也会导致库存单行扣减压力
- 整个流程都是串行处理,如果下游接口响应耗时过高,会导致服务雪崩问题。
所以还需要对下单链路进行处理
- 库存单行扣减优化
- 接口部分调用串行转成并行处理,降低接口响应耗时,提升服务处理速度
方案
架构升级:
- 库存分层设计:
- Redis预扣(90%库存):通过Lua脚本保证原子扣减,提升下单库存性能
- DB兜底(10%库存):故障熔断时启用,结合库存校准机制 防超卖(日志回溯+定时校准)。
- 支付回调异步化:
- 临时表削峰:支付成功后写入待扣减记录,定时任务批量处理DB库存。
- 并行化链路:优惠券核销、积分计算等模块异步执行,接口响应下降。
对于库存的扣减优化,主要有两点:
- 下单扣减库存从DB扣改为redis扣,具体处理方式为:
- a. 下单减库存,取全量库存的90%放入Redis进行扣减,确保在Redis不可用的情况下,能有部分数据库库存 承接流量等待库存校准完成。
- b. Redis扣减失败会通过数据库进行扣减,失败达到阈值触发库存校准,并关闭热点标。
- c. 库存校准:每次缓存库存操作会记录日志,用于校准数据库库存,避免因Redis超时重试等情况产生的超卖少卖。
- 支付回调后扣减进入临时表缓冲
- 支付回调存在了QPS不稳定的风险,并且因为涉及到订单状态的变更,也不能进行限流操作,但是因为此处库存不影响前台项目的销售,所以接受一定程度的延迟,在支付回调过程中,将需要扣减的冻结库存暂时写入到一张临时表中,通过定时任务的方式做批量化处理,既起到了削峰的作用,也降低了库存的操作频次,大大降低了DB的热点数据问题。
同时,为了系统性地提高整体的读写并发度,我们做了如下梳理和优化
- 应用节点(链路)过长
- 强弱依赖梳理,提供降级开关
- 缩短RT时长,提升下单链路的各个接口性能
- 规整服务告警和异常处理机制,尽量收缩核心链路范围,保证爆发期主链路的服务可用率达到SLA(下图是focus主链路异常处理和SLA监控的梳理逻辑)
- 数据库
- sql慢查询优化
- DB主从数据同步延迟:下单链路查主库
- 数据库表优化:合表减少减少查表次数等
- 大事务:将数据库查询工作和rpc调用移除事务
- 扫描资源消耗过大的定时任务做提前预案,抢票时提前降级:改用databus消费
- 锁等待:避免非抢票相关的链路导致数据库加锁
- 数据库隔离级别:RR改为RC
为什么在高并发场景下使用RC更合适?
首先,RC 在加锁的过程中,是不需要添加间隙锁Gap Lock和临键锁Next-Key Lock 的,只对要修改的记录添加行级锁就行了。这就使得并发度要比 RR 高很多。另外,因为 RC 还支持"半一致读",可以大大的减少了更新语句时行锁的冲突;对于不满足更新条件的记录,可以提前释放锁,提升并发度。
减少死锁
因为RR这种事务隔离级别会增加Gap Lock和 Next-Key Lock,这就使得锁的粒度变大,那么就会使得死锁的概率增大。
带来的问题
首先使用 RC 之后,就需要自己解决幻读的问题。还有就是使用 RC 的时候,不能使用statement格式的 binlog,这种影响其实可以忽略不计了,因为MySQL是在5.1.5版本开始支持row的、在5.1.8版本中开始支持mixed,后面这两种可以代替 statement格式。
总结
通过三阶段的优化,B站票务系统在高并发场景下的性能和稳定性得到了显著提升。从最初的同步事务处理到异步下单,再到基于Redis的库存优化方案,最终实现了系统的稳定性和性能的大幅提升。这些优化措施不仅解决了高并发场景下的性能瓶颈,还提升了用户体验,为业务的持续发展提供了坚实的技术支持。
本文原文来自CSDN