LOOK->大规模高性能选座抢票背后的设计

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

一、背景介绍

随着疫情的消散,演唱会亏快速强劲复苏,各类演出层出不穷,越来越多的演出开启选座购票满足用户的自主选座需求。主流演出售卖平台的选座功能不仅面向中小场馆类的剧院演出,还面向大型体育赛事、大型 演唱会等超大型场馆(如北京鸟巢近 10 万座,香港红馆等)。选座类型抢票的主流特点是“选”的操作,由于“选”的可视化以及超大场馆在数据量上对各大演出平台都是很大的挑战。那么本文就是从技术角度和大家来探讨下如何支撑超大规模场馆的高性能选座,同时通过本文的一些技术方案希望可以对大家在一些高并发实践中提供帮助,并且一起进步。

二、核心问题

首先我们先说下一个老生常谈的电商秒杀场景,比如双十一某个手机品牌方做大促,一般情况下,一场手机只有几个型号, 比如中低档、高性能档、旗舰等,这时候我们只需要处理好对应商品的库存即可。但是对于选座类抢票而言,每一个场次的所有的每一个座位都可以认为是一个不同的商品,因为在演唱会中座位的位置有很大的差异(其实就可以理解为中低档、高性能档、旗舰机不同的价格),并且场馆大小不一,大的鸟巢有 10w 座 位。也即是说一个选座类抢票就涉及 10 万级别的商品,如果多个项目同时开抢,整体数量会很庞大。目前主流演出平台侧的商品维度是票档,座位并不是商品粒度,其实演唱会抢票比常见电商链路更复杂涉及电商、选座、票务产销,是对平台系统整体能力的考验。

先来看看整个选座购票的流程:我们以一个简单的演出选座项目为例。

  1. 用户在APP首先打开要抢演唱会的项目

image.png

  1. 点击选座购买,打开选座页面,查看座位图及票档

image.png

  1. 选择一个PriceZone(座位具体区域),选择喜欢的座位,点击确认选座

image.png

  1. 进入下单页面,填写手机号收货地址,创建订单

image.png

  1. 提交订单完成付款、出票。

分析:

其中,2、3、4 步骤都与选座相关。从流程上看,选座的核心关键技术在于:

①座位图的如何快熟渲染到页面。快速加载其实就是选座页面的读能力。选座页面需要下载座位底图、 座位基础信息(排号等等)来做渲染,同时需要票档、该场次每个座位的状态,来决定是可售 还是锁定还是已经售出。 对于大型场馆座位 5 万-10 万,渲染一个选座页面需要的数据量都很大。
②高并发。由于热门演出票的稀缺性,同时抢票的人数可能达到几十万。如何支撑如此高 的并发和吞吐是一大考验。
③座位状态更新的及时性 当某个座位售出后,需要及时更新座位状态。
④抢票体验:抢票时热门的看台某个座位可能几十个人并发去抢,如何尽量提升用户的体验,尽量让更多用户一次性购买成功,体验更好。

三、怎么去应对高性能选座,需要做些什么?

3.1 动静结合

选座的瓶颈数据量“首当其冲”。从逻辑上讲,一个座位完整的展现到用户面前,需要包含座位的看台、排号、价格、售卖状态等信息,其中 看台、排号等等是不变的,并可提前预知的; 售卖状态等时根据项目的进行会动态变化的。所以把选座的数据拆分为动态、静态数据。对于大型场馆如 10 万场馆,用户打开一个选座页,座位的静态数据(座位 id,票价 id,是否固定套 票,坐标 x,y,和舞台角度,哪个看台,几排几号等等),这些数据是提前知道的大概 17M 左右。再加上动态数据如票档状态、颜色、看台状态、座位状态,10w 场馆大概 3M 左右。也就是说如果不做 处理用户仅仅打开选座页就需要 20M 以上的数据量。如果选座数据存储在 cos 上,如果每人下 载 17M,10w 人同时抢票则需要 1700G 带宽,带宽成本很高。为了解决静态文件访问速度问题, 将静态数据从 cos 直接接入到 cdn。同时为了保障数据最新,静态数据采用版本控制

3.2 静态数据的预加载

如果使用上边的方法那么在大型演唱会抢票的时候带宽峰值很高,为了降低峰值提升用户体验,这时候我们在客户端可以引入了静态数据预加载。静态信息再结合预加载的处理,为处理大数据量的座位信息提供了时间上的余量,用户在打开选座页这时候优先显示静态信息,可有效降低用户等待时间,提升用户的体验。
通过BI分析大部分用户的行为习惯,遵循二八原则,确定预加载的场次类型,提高预加载的命中率。

未命名绘图.png

预加载+预解析,完成了绘制基本场馆信息的数据准备,再将数据提前与画座 View 绑定, 预渲染 View 进而达到选座页秒开效果

3.3 座位数据压缩

image.png

动态、静态数据量大制约了我们实现高吞吐,同时也浪费了服务带宽和用户流量。所以需 要压缩,压缩到一定的可接受的范围。只有数据量足够小,才有办法做到高吞吐。

1)静态数据编码 在处理大数据量的座位(例如十万级)仅有静态数据的预加载往往是不足的,预加载并没有从根本上处理座位数据量大的问题,同时对于类似赛事这种多日期多场次的场景,由于预加载的使用存在缓存量的控制,比如可能某一天的缓存数据可能不同,往往影响预加载的命中率。 而数据重编码和数据压缩的使用才是源头解决问题的有效思路。
座位数据的重编,传统的方式有 xml 和 json 格式,但是从在一定的风险,但是重编不仅可以有效压缩数据大小,还对数据安 全提供了保障,即使被拿到了接口数据,因为缺乏数据编码的协议,也无法获取有效的原始信息。
2)座位静态数据压缩整体框架 ,结合高效二进制编码方案进行座位数据的重新编码,再使用通用的无损压缩进一步缩小数据体积,从而减少了座位数据的网络传输时间,从根本上解决大数据传输导致的时延问题,比如先用Hessian 转化为二进制,然后再进行压缩。

3)座位动态数据的编码处理 a)动态数据的难点选座动态接口主要涉及票档情况、看台情况、座位状态。数据量最大的是座位状态。以一 个 10 万座位的场馆为例,每个座位都要返回一个状态供前端渲染。采用座位 id 和状态键值对的 方式,由于座位较多使得整个返回结果过大,10 万座位的场馆返回 2M 以上的数据。如果打开 一个选座页需要吞吐 2M 数据量的化,接口基本不可用了。之前的策略是按照分组策略,10 万 大概会分 10 个组,这样每个请求大概 200k 数据量,这样才能达到接口基本可用,不过端上需 要请求 10 次才能拿到整个场次的状态,可想而知性能会有多大影响。假如 10 万座位的场馆,10 万峰值抢票,那么仅仅这个接口就需要 200 万的 qps,所以肯定会逢抢必挂。
b)方案
如何把返回的数据减小,采用一个尽量简洁的方案,同时调用一次返回 整个场馆座位状态,是我们努力的方向。 数据量大主要是因为有很多冗余的座位 id。有没有办法不依赖这些座位 id?既然我们做的 动静分离,静态数据里已经涵盖了座位 id,我们动态接口里只需要对应的返回状态即可,即按 照静态里面的顺序返回座位状态。同时我们把返回结果进行简单的相邻状态合并将进一步降低 返回结果大小。假设用户选座座位符合正态分布概率,平均长度 10w 座位平均返回不到 50k。
最后我们可以转化为二进制流进一步压缩网络传输。

3.4 缓存银弹

1)缓存
面向这么高的 TPS,Redis 是不二首选。采用 Redus + 本地缓存,来支撑如此高的数据峰值。 提到 Redis,提一下我们这边的一些策略。 用户进到选座页是一个个的看台,我们设计了一级 stand cache,即看台级别的 cache;用户会进行选座位,我们又加了一级 seat cache,即为座位粒度的 cache。两级 cache 保障用户查看台和用户下单选座都能命中缓存。standcache 是热点 key,从选座的场景是允许数据非准实时 的,所以引入了 guava localcache这里也可以用caffine 来增加吞吐,以此降低对 Redis 的读压力。
2)保护下游系统
目前采用的策略是 对下游的调用采用加锁,Redis 全局锁。拿到锁的才去请求票务底层数据。拿到锁的进程去更新 Redis 缓存。其实从这里看对 Redis 的写还是 qps 比较小的,但是每次争抢锁对 Redis 的读还是不算太小。通过采用本地的锁 + 随机透传,来减少 Redis 锁耗费的读qps。为了 对下游依赖做降级,增加了数据快照,每次对下游的调用记录数据快照。当某次调用失败采用之前的快照进行补偿。
3)保障数据更新及时
由于我们采用了 Redis 全局锁,可以按照秒级控制下游调用。调用采用异步触发。最短 1s 内 会触发我们发起对下游的调用。如果我们想最大化利用票务库存能力,给用户的延迟在 1s 以 内,我们有一些策略。拿到锁的线程 1s 内调用数据更新任务,在数据更新任务里做一些策略, 1s 内是发起 1 次还是多次对票务的调用,调用越多 Redis 更新越及时。由于用户有一定的选座动作,一般情况下 500ms 的延迟用户基本无任何感知的。
4)缓存预热 预热一下缓存,对用户体验和系统性能很有帮助。抢票类项目采用一定的策略做自动化预热。

3.5 HA保证

1)安全 从上文看座位数据做了编码和加密,同时存储路径做了混淆,保障不到开抢时间座位数据无法被破解,保障了选座数据的安全性。此外选座页布局防控策略,只有真正需要点击座位才能完成下单(一定时间内会记录点击坐标和点击ip的关联关系)防止机刷、防止绕过选座直接下单。通过类似策略降低了选座的无效流量, 提高了稳定性。
2)容灾
选座主要在以下几个方面做了容灾。静态数据存储在cos 上,目前采用跨地区存储容灾;redis 缓存采用主备缓存,出故障时可以做切换;服务端在 pc 和无线做了集群隔离。

四:对现有业务一些思考:

  1. 怎么应对高并发选座?

image.png

前提:发布机制引入,发布的时候会把座位库存数据全部从DB缓存一份到Redis。此时所有座位信息DB和Redis数据同步,同时提供了本地缓存机制(将redis中所有座位库存数据在用到的时候缓存到本地),依靠redis缓存和本地缓存,很大程度上提高了处理速度,但是也存在相应问题不同机器本地缓存数据不一致,这个怎么解决是要考虑的问题,下边会讲到。

  1. 使用了本地缓存,如果两个用户选择了相同座位请求打在两台机器上这个怎么处理?

针对上边1留下的问题,这里给出相应解决方案 ,先看图理解下:

流程简化图:

image.png

具体细节图:

image.png

可以看出在选座的过程中会有任务概念,每个选座都会成成一个任务,这个任务就是用户选座的一个行为,各个服务器之间 作为数据的同步就是通过这个任务来实现的,具体流程如下:

(1)管理人员B端上单,准备好对应项目场次座位数据然后发布,发布过程中同时会把座位相关数据缓存到Redis,后续用户选座行为都无和数据库交互,直到用户真正下单。

(2) 用户客户端选座,选座的过程中会从Redis中查询座位库存数据缓存到本地,同时生成本机选座任务提交到Redis选座队列里,并且记录当前任务到本地缓存。

(3)每个用户来选座请求有可能会打到不同的服务器上,并且也有可能选择相同的场次以及座位,这时候就要靠 (4)中的机制来保证同一个座位不被重复选中。

(4)从(2)中可以知道,每个选座的操作都会生成一个任务放到Redis队列里 如上图3所示,每台服务器在执行选座的时候都会先从redis选座队列里把全部选座任务在自己机器上执行一边,同时执行的时候也有一个版本的概念,历史已经执行过的会被过滤掉。

(5)这时候就严格按照redis 选座队列中任务的顺序来执行,如果有人选了重复的座位,肯定是队列里第一个先选会先执行,并且把对应座位占用掉,后续另外一个选择同一个座位的任务这时候再选的时候发现选择的座位已经被占用所以就选座失败了。这里有一个细节点,在选座的时候,我们会把选座队列里所有的任务都执行掉,可能执行的过程中,某个任务失败了,那么这个选座的操作怎么去判定是否成功呢? 答案是我们只关心当前服务器所接受的选座行为是否成功来座位是否选座成功的结果,执行其他任务的目的只是同步我本地缓存座位数据,即使不成功,下次选座请求打到这一台机器上的时候还会从新再拉取到那个任务,目的就是保证各个服务之间,座位数据的最终一致性。

对于本机任务执行失败的场景,在(2)中我们也有提到,我们会记录本机提交的任务到本地缓存中,如果一直没有对应结果操作(confirm,cancel)我们会兜底之心对应cancel。

最后:可能业务上大家都有很大的差异,但是从技术层面上看,对于一类问题的解决方案就和业务没有具体关联了,就拿四中说一些点,在各种大流量的项目以及业务中,这种方式同样适用,比如电商场景的库存等,其实票务也属于电商,而且在一定程度上会比电商业务更为复杂,所以沉淀一些通用型的业务或者架构设计方案还是很有帮助的。

往期经典:

【干货】使用Canal 解决数据同步场景中的疑难杂症!!!

干货】常见库存设计方案-各种方案对比总有一个适合你

JYM来一篇消息队列-Kafaka的干货吧!!!

设计模式:

JYM 设计模式系列- 单例模式,适配器模式,让你的代码更优雅!!!

JYM 设计模式系列- 责任链模式,装饰模式,让你的代码更优雅!!!

JYM 设计模式系列- 策略模式,模板方法模式,让你的代码更优雅!!!

JYM 设计模式系列-工厂模式,让你的代码更优雅!!!

Spring相关:

Spring源码解析-老生常谈Bean ⽣命周期 ,但这个你值得看!!!

Spring 源码解析-JYM你值得拥有,从源码角度看bean的循环依赖!!!

Spring源码解析-Spring 事务

本文正在参加「金石计划」

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYqjho2H' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片